pax_global_header00006660000000000000000000000064146163744730014531gustar00rootroot0000000000000052 comment=1b23e57257a32ee0ab0f2b10fdb04af3ed26774c mpire-2.10.2/000077500000000000000000000000001461637447300127275ustar00rootroot00000000000000mpire-2.10.2/.github/000077500000000000000000000000001461637447300142675ustar00rootroot00000000000000mpire-2.10.2/.github/workflows/000077500000000000000000000000001461637447300163245ustar00rootroot00000000000000mpire-2.10.2/.github/workflows/github-pages.yml000066400000000000000000000017341461637447300214330ustar00rootroot00000000000000name: Docs on: push jobs: build-n-publish: name: Build and publish documentation to Github runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.6" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine rich pip install .[dashboard] pip install .[dill] pip install .[docs] - name: Build documentation run: | sphinx-versioning build -r master ./docs/ ./docs/_build/html/ - name: Publish documentation to Github if: startsWith(github.ref, 'refs/tags') uses: peaceiris/actions-gh-pages@v3.8.0 with: deploy_key: ${{ secrets.DEPLOY_GITHUB_PAGES_KEY }} external_repository: sybrenjansen/sybrenjansen.github.io publish_branch: main publish_dir: ./docs/_build/html/ destination_dir: mpire mpire-2.10.2/.github/workflows/python-package.yml000066400000000000000000000027751461637447300217740ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Build on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-20.04, windows-latest, macos-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest pip install .[dashboard] pip install .[dill] pip install .[testing] - name: Set ulimit for macOS if: matrix.os == 'macos-latest' run: | ulimit -a ulimit -n 1024 - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest timeout-minutes: 30 run: | pytest -v -o log_cli=true -s mpire-2.10.2/.github/workflows/python-publish.yml000066400000000000000000000014121461637447300220320ustar00rootroot00000000000000name: Upload Python Package on: push jobs: build-n-publish: name: Build and publish Python distributions to PyPI runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.6" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build a binary wheel and a source tarball run: | python setup.py sdist python setup.py bdist_wheel - name: Publish distribution to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} mpire-2.10.2/.gitignore000066400000000000000000000001261461637447300147160ustar00rootroot00000000000000.idea __pycache__ build _build dist *.egg-info .eggs .pytest_cache # MacOS .DS_store mpire-2.10.2/LICENSE000066400000000000000000000020561461637447300137370ustar00rootroot00000000000000MIT License Copyright (c) 2023 Sybren Jansen 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. mpire-2.10.2/MANIFEST.in000066400000000000000000000004011461637447300144600ustar00rootroot00000000000000include README.rst include LICENSE recursive-include mpire/dashboard/static *.eot *.svg *.ttf *.woff *.woff2 *.js *.css recursive-include mpire/dashboard/templates *.html include requirements.txt include setup.cfg include MANIFEST.in include mpire/py.typed mpire-2.10.2/README.rst000066400000000000000000000275351461637447300144320ustar00rootroot00000000000000MPIRE (MultiProcessing Is Really Easy) ====================================== |Build status| |Docs status| |Pypi status| |Python versions| .. |Build status| image:: https://github.com/sybrenjansen/mpire/workflows/Build/badge.svg?branch=master :target: https://github.com/sybrenjansen/mpire/actions/workflows/python-package.yml :alt: Build status .. |Docs status| image:: https://github.com/sybrenjansen/mpire/workflows/Docs/badge.svg?branch=master :target: https://sybrenjansen.github.io/mpire/ :alt: Documentation .. |PyPI status| image:: https://img.shields.io/pypi/v/mpire :target: https://pypi.org/project/mpire/ :alt: PyPI project page .. |Python versions| image:: https://img.shields.io/pypi/pyversions/mpire :target: https://pypi.org/project/mpire/ :alt: PyPI project page ``MPIRE``, short for MultiProcessing Is Really Easy, is a Python package for multiprocessing. ``MPIRE`` is faster in most scenarios, packs more features, and is generally more user-friendly than the default multiprocessing package. It combines the convenient map like functions of ``multiprocessing.Pool`` with the benefits of using copy-on-write shared objects of ``multiprocessing.Process``, together with easy-to-use worker state, worker insights, worker init and exit functions, timeouts, and progress bar functionality. Full documentation is available at https://sybrenjansen.github.io/mpire/. Features -------- - Faster execution than other multiprocessing libraries. See benchmarks_. - Intuitive, Pythonic syntax - Multiprocessing with ``map``/``map_unordered``/``imap``/``imap_unordered``/``apply``/``apply_async`` functions - Easy use of copy-on-write shared objects with a pool of workers (copy-on-write is only available for start method ``fork``) - Each worker can have its own state and with convenient worker init and exit functionality this state can be easily manipulated (e.g., to load a memory-intensive model only once for each worker without the need of sending it through a queue) - Progress bar support using tqdm_ (``rich`` and notebook widgets are supported) - Progress dashboard support - Worker insights to provide insight into your multiprocessing efficiency - Graceful and user-friendly exception handling - Timeouts, including for worker init and exit functions - Automatic task chunking for all available map functions to speed up processing of small task queues (including numpy arrays) - Adjustable maximum number of active tasks to avoid memory problems - Automatic restarting of workers after a specified number of tasks to reduce memory footprint - Nested pool of workers are allowed when setting the ``daemon`` option - Child processes can be pinned to specific or a range of CPUs - Optionally utilizes dill_ as serialization backend through multiprocess_, enabling parallelizing more exotic objects, lambdas, and functions in iPython and Jupyter notebooks. MPIRE is tested on Linux, macOS, and Windows. For Windows and macOS users, there are a few minor known caveats, which are documented in the Troubleshooting_ chapter. .. _benchmarks: https://towardsdatascience.com/mpire-for-python-multiprocessing-is-really-easy-d2ae7999a3e9 .. _multiprocess: https://github.com/uqfoundation/multiprocess .. _dill: https://pypi.org/project/dill/ .. _tqdm: https://tqdm.github.io/ .. _Troubleshooting: https://sybrenjansen.github.io/mpire/troubleshooting.html Installation ------------ Through pip (PyPi): .. code-block:: bash pip install mpire MPIRE is also available through conda-forge: .. code-block:: bash conda install -c conda-forge mpire Getting started --------------- Suppose you have a time consuming function that receives some input and returns its results. Simple functions like these are known as `embarrassingly parallel`_ problems, functions that require little to no effort to turn into a parallel task. Parallelizing a simple function as this can be as easy as importing ``multiprocessing`` and using the ``multiprocessing.Pool`` class: .. _embarrassingly parallel: https://en.wikipedia.org/wiki/Embarrassingly_parallel .. code-block:: python import time from multiprocessing import Pool def time_consuming_function(x): time.sleep(1) # Simulate that this function takes long to complete return ... with Pool(processes=5) as pool: results = pool.map(time_consuming_function, range(10)) MPIRE can be used almost as a drop-in replacement to ``multiprocessing``. We use the ``mpire.WorkerPool`` class and call one of the available ``map`` functions: .. code-block:: python from mpire import WorkerPool with WorkerPool(n_jobs=5) as pool: results = pool.map(time_consuming_function, range(10)) The differences in code are small: there's no need to learn a completely new multiprocessing syntax, if you're used to vanilla ``multiprocessing``. The additional available functionality, though, is what sets MPIRE apart. Progress bar ~~~~~~~~~~~~ Suppose we want to know the status of the current task: how many tasks are completed, how long before the work is ready? It's as simple as setting the ``progress_bar`` parameter to ``True``: .. code-block:: python with WorkerPool(n_jobs=5) as pool: results = pool.map(time_consuming_function, range(10), progress_bar=True) And it will output a nicely formatted tqdm_ progress bar. MPIRE also offers a dashboard, for which you need to install additional dependencies_. See Dashboard_ for more information. .. _dependencies: https://sybrenjansen.github.io/mpire/install.html#dashboard .. _Dashboard: https://sybrenjansen.github.io/mpire/usage/dashboard.html Shared objects ~~~~~~~~~~~~~~ Note: Copy-on-write shared objects is only available for start method ``fork``. For ``threading`` the objects are shared as-is. For other start methods the shared objects are copied once for each worker, which can still be better than once per task. If you have one or more objects that you want to share between all workers you can make use of the copy-on-write ``shared_objects`` option of MPIRE. MPIRE will pass on these objects only once for each worker without copying/serialization. Only when you alter the object in the worker function it will start copying it for that worker. .. code-block:: python def time_consuming_function(some_object, x): time.sleep(1) # Simulate that this function takes long to complete return ... def main(): some_object = ... with WorkerPool(n_jobs=5, shared_objects=some_object) as pool: results = pool.map(time_consuming_function, range(10), progress_bar=True) See shared_objects_ for more details. .. _shared_objects: https://sybrenjansen.github.io/mpire/usage/workerpool/shared_objects.html Worker initialization ~~~~~~~~~~~~~~~~~~~~~ Workers can be initialized using the ``worker_init`` feature. Together with ``worker_state`` you can load a model, or set up a database connection, etc.: .. code-block:: python def init(worker_state): # Load a big dataset or model and store it in a worker specific worker_state worker_state['dataset'] = ... worker_state['model'] = ... def task(worker_state, idx): # Let the model predict a specific instance of the dataset return worker_state['model'].predict(worker_state['dataset'][idx]) with WorkerPool(n_jobs=5, use_worker_state=True) as pool: results = pool.map(task, range(10), worker_init=init) Similarly, you can use the ``worker_exit`` feature to let MPIRE call a function whenever a worker terminates. You can even let this exit function return results, which can be obtained later on. See the `worker_init and worker_exit`_ section for more information. .. _worker_init and worker_exit: https://sybrenjansen.github.io/mpire/usage/map/worker_init_exit.html Worker insights ~~~~~~~~~~~~~~~ When your multiprocessing setup isn't performing as you want it to and you have no clue what's causing it, there's the worker insights functionality. This will give you insight in your setup, but it will not profile the function you're running (there are other libraries for that). Instead, it profiles the worker start up time, waiting time and working time. When worker init and exit functions are provided it will time those as well. Perhaps you're sending a lot of data over the task queue, which makes the waiting time go up. Whatever the case, you can enable and grab the insights using the ``enable_insights`` flag and ``mpire.WorkerPool.get_insights`` function, respectively: .. code-block:: python with WorkerPool(n_jobs=5, enable_insights=True) as pool: results = pool.map(time_consuming_function, range(10)) insights = pool.get_insights() See `worker insights`_ for a more detailed example and expected output. .. _worker insights: https://sybrenjansen.github.io/mpire/usage/workerpool/worker_insights.html Timeouts ~~~~~~~~ Timeouts can be set separately for the target, ``worker_init`` and ``worker_exit`` functions. When a timeout has been set and reached, it will throw a ``TimeoutError``: .. code-block:: python def init(): ... def exit_(): ... # Will raise TimeoutError, provided that the target function takes longer # than half a second to complete with WorkerPool(n_jobs=5) as pool: pool.map(time_consuming_function, range(10), task_timeout=0.5) # Will raise TimeoutError, provided that the worker_init function takes longer # than 3 seconds to complete or the worker_exit function takes longer than # 150.5 seconds to complete with WorkerPool(n_jobs=5) as pool: pool.map(time_consuming_function, range(10), worker_init=init, worker_exit=exit_, worker_init_timeout=3.0, worker_exit_timeout=150.5) When using ``threading`` as start method MPIRE won't be able to interrupt certain functions, like ``time.sleep``. See timeouts_ for more details. .. _timeouts: https://sybrenjansen.github.io/mpire/usage/map/timeouts.html Benchmarks ---------- MPIRE has been benchmarked on three different benchmarks: numerical computation, stateful computation, and expensive initialization. More details on these benchmarks can be found in this `blog post`_. All code for these benchmarks can be found in this project_. In short, the main reasons why MPIRE is faster are: - When ``fork`` is available we can make use of copy-on-write shared objects, which reduces the need to copy objects that need to be shared over child processes - Workers can hold state over multiple tasks. Therefore you can choose to load a big file or send resources over only once per worker - Automatic task chunking The following graph shows the average normalized results of all three benchmarks. Results for individual benchmarks can be found in the `blog post`_. The benchmarks were run on a Linux machine with 20 cores, with disabled hyperthreading and 200GB of RAM. For each task, experiments were run with different numbers of processes/workers and results were averaged over 5 runs. .. image:: images/benchmarks_averaged.png :width: 600px :alt: Average normalized bechmark results .. _blog post: https://towardsdatascience.com/mpire-for-python-multiprocessing-is-really-easy-d2ae7999a3e9 .. _project: https://github.com/sybrenjansen/multiprocessing_benchmarks Documentation ------------- See the full documentation at https://sybrenjansen.github.io/mpire/ for information on all the other features of MPIRE. If you want to build the documentation yourself, please install the documentation dependencies by executing: .. code-block:: bash pip install mpire[docs] or .. code-block:: bash pip install .[docs] Documentation can then be build by using Python <= 3.9 and executing: .. code-block:: bash python setup.py build_docs Documentation can also be build from the ``docs`` folder directly. In that case ``MPIRE`` should be installed and available in your current working environment. Then execute: .. code-block:: bash make html in the ``docs`` folder. mpire-2.10.2/bin/000077500000000000000000000000001461637447300134775ustar00rootroot00000000000000mpire-2.10.2/bin/mpire-dashboard000066400000000000000000000027701461637447300164710ustar00rootroot00000000000000#!/usr/bin/env python import argparse import signal from typing import Sequence from mpire.dashboard import start_dashboard def get_port_range() -> Sequence: """ :return: port range """ def _port_range(range_str) -> Sequence: n1, n2 = map(int, range_str.split('-')) if len(range(n1, n2)) < 2: raise ValueError return range(n1, n2) parser = argparse.ArgumentParser(description='MPIRE Dashboard') parser.add_argument('--port-range', dest='port_range', required=False, default=range(8080, 8100), type=_port_range, help='Port range for starting a dashboard. The range should accommodate at least two ports: ' 'one for the webserver and one for the Python Manager server. Example: 6060-6080 will be ' 'converted to `range(6060, 6080)`. Default: `range(8080, 8100)`.') return parser.parse_args().port_range if __name__ == '__main__': # Obtain port range port_range = get_port_range() # Start a dashboard print("Starting MPIRE dashboard...") dashboard_details = start_dashboard(port_range) # Print some details on how to connect print() print("MPIRE dashboard started on http://localhost:{}".format(dashboard_details['dashboard_port_nr'])) print("Server is listening on {}:{}".format(dashboard_details['manager_host'], dashboard_details['manager_port_nr'])) print("-" * 50) signal.pause() mpire-2.10.2/docs/000077500000000000000000000000001461637447300136575ustar00rootroot00000000000000mpire-2.10.2/docs/Makefile000066400000000000000000000166661461637447300153360ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: 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 " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp 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." .PHONY: qthelp 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/lexsys.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/lexsys.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/lexsys" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/lexsys" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex 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)." .PHONY: latexpdf 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." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck 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." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." mpire-2.10.2/docs/_static/000077500000000000000000000000001461637447300153055ustar00rootroot00000000000000mpire-2.10.2/docs/_static/css/000077500000000000000000000000001461637447300160755ustar00rootroot00000000000000mpire-2.10.2/docs/_static/css/custom.css000066400000000000000000000041141461637447300201210ustar00rootroot00000000000000.strike { text-decoration: line-through; } /* From theme.css, but .section has been replaced by section to work around the new change of
to
, which for some reason happened */ .rst-content section ul { list-style:disc; line-height:24px; margin-bottom:24px } .rst-content section ul li { list-style:disc; margin-left:24px } .rst-content section ul li p:last-child, .rst-content section ul li ul { margin-top:0; margin-bottom:0 } .rst-content section ul li li { list-style:circle } .rst-content section ul li li li { list-style:square } .rst-content section ul li ol li { list-style:decimal } .rst-content section ol { list-style:decimal; line-height:24px; margin-bottom:24px } .rst-content section ol li { list-style:decimal; margin-left:24px } .rst-content section ol li p:last-child, .rst-content section ol li ul { margin-bottom:0 } .rst-content section ol li ul li { list-style:disc } .rst-content section ol.loweralpha, .rst-content section ol.loweralpha>li { list-style:lower-alpha } .rst-content section ol.upperalpha, .rst-content section ol.upperalpha>li { list-style:upper-alpha } .rst-content section ol li>*, .rst-content section ul li>* { margin-top:12px; margin-bottom:12px } .rst-content section ol li>:first-child, .rst-content section ul li>:first-child { margin-top:0 } .rst-content section ol li>p, .rst-content section ol li>p:last-child, .rst-content section ul li>p, .rst-content section ul li>p:last-child { margin-bottom:12px } .rst-content section ol li>p:only-child, .rst-content section ol li>p:only-child:last-child, .rst-content section ul li>p:only-child, .rst-content section ul li>p:only-child:last-child { margin-bottom:0 } .rst-content section ol li>ol, .rst-content section ol li>ul, .rst-content section ul li>ol, .rst-content section ul li>ul { margin-bottom:12px } .rst-content section ol.simple li>*, .rst-content section ol.simple li ol, .rst-content section ol.simple li ul, .rst-content section ul.simple li>*, .rst-content section ul.simple li ol, .rst-content section ul.simple li ul { margin-top:0; margin-bottom:0 }mpire-2.10.2/docs/changelog.rst000066400000000000000000000415441461637447300163500ustar00rootroot00000000000000Changelog ========= 2.10.2 ------ *(2024-05-07)* * Function details in ``progress_bar.py`` are only obtained when the dashboard is running (`#128`_) * Obtaining the user name is now put in a try-except block to prevent MPIRE from crashing when the user name cannot be obtained. which can happen when running in a container as a non-root user (`#128`_) .. _#128: https://github.com/sybrenjansen/mpire/issues/128 2.10.1 ------ *(2024-03-19)* * Fixed a bug in the timeout handler where the cache dictionary could be changed during iteration (`#123`_) * Fixed an authentication error when using a progress bar or insights in a spawn or forkserver context when using dill (`#124`_) .. _#123: https://github.com/sybrenjansen/mpire/issues/123 .. _#124: https://github.com/sybrenjansen/mpire/issues/124 2.10.0 ------ *(2024-02-19)* * Added support for macOS (`#27`_, `#79`_, `#91`_) - Fixes memory leaks on macOS - Reduced the amount of semaphores used - Issues a warning when ``cpu_ids`` is used on macOS * Added :meth:`mpire.dashboard.set_stacklevel` to set the stack level in the dashboard. This influences what line to display in the 'Invoked on line' section. (`#118`_) * Use function details from the `__call__` method on the dashboard in case the callable being executed is a class instance (`#117`_) * Use (global) average rate for the estimate on the dashboard when smoothing=0 (`#117`_) * Make it possible to reuse the same `progress_bar_options` without raising warnings (`#117`_) * Removed deprecated `progress_bar_position` parameter from the `map` functions. Use `progress_bar_options['position']` instead (added since v2.6.0) .. _#27: https://github.com/sybrenjansen/mpire/issues/27 .. _#79: https://github.com/sybrenjansen/mpire/issues/79 .. _#91: https://github.com/sybrenjansen/mpire/issues/91 .. _#117: https://github.com/sybrenjansen/mpire/pull/117 .. _#118: https://github.com/sybrenjansen/mpire/pull/118 2.9.0 ----- *(2024-01-08)* * Added support for the ``rich`` progress bar style (`#96`_) * Added the option to only show progress on the dashboard. (`#107`_) * Progress bars are now supported on Windows when using threading as start method. * Insights now also work when using the ``forkserver`` and ``spawn`` start methods. (`#104`_) * When using insights on Windows the arguments of the top 5 longest tasks are now available as well. * Fixed deprecated ``escape`` import from ``flask`` by importing directly from ``markupsafe``. (`#106`_) * Fixed :meth:`mpire.dashboard.start_dashboard` freeze when there are no two ports available. (`#112`_) * Added :meth:`mpire.dashboard.shutdown_dashboard` to shutdown the dashboard. * Added ``py.typed`` file to prompt ``mypy`` for type checking. (`#108`_) .. _#96: https://github.com/sybrenjansen/mpire/issues/96 .. _#107: https://github.com/sybrenjansen/mpire/pull/107 .. _#104: https://github.com/sybrenjansen/mpire/issues/104 .. _#106: https://github.com/sybrenjansen/mpire/issues/106 .. _#112: https://github.com/sybrenjansen/mpire/issues/112 .. _#108: https://github.com/sybrenjansen/mpire/pull/108 2.8.1 ----- *(2023-11-08)* * Excluded the ``tests`` folder from MPIRE distributions (`#89`_) * Added a workaround for semaphore leakage on macOS and fixed a bug when working in a fork context while the system default is spawn (`#92`_) * Fix progressbar percentage on dashboard (`#101`_) * Fixed a bug where starting multiple `apply_async` tasks with a task timeout didn't interrupt all tasks when the timeout was reached (`#98`_) * Add testing python 3.12 to workflow and drop 3.6 and 3.7 (`#102`_) .. _#89: https://github.com/sybrenjansen/mpire/issues/89 .. _#92: https://github.com/sybrenjansen/mpire/issues/92 .. _#98: https://github.com/sybrenjansen/mpire/issues/98 .. _#101: https://github.com/sybrenjansen/mpire/pull/101 .. _#102: https://github.com/sybrenjansen/mpire/pull/102 2.8.0 ----- *(2023-08-16)* * Added support for Python 3.11 (`#67`_) .. _#67: https://github.com/sybrenjansen/mpire/issues/67 2.7.1 ----- *(2023-04-14)* * Transfered ownership of the project from `Slimmer AI` to `sybrenjansen` 2.7.0 ----- *(2023-03-17)* * Added the :meth:`mpire.WorkerPool.apply` and :meth:`mpire.WorkerPool.apply_async` functions (`#63`_) * When inside a Jupyter notebook, the progress bar will not automatically switch to a widget anymore. ``tqdm`` cannot always determine with certainty that someone is in a notebook or, e.g., a Jupyter console. Another reason is to avoid the many errors people get when having widgets or javascript disabled. See :ref:`progress_bar_style` for changing the progress bar to a widget (`#71`_) * The :meth:`mpire.dashboard.connect_to_dashboard` function now raises a `ConnectionRefused` error when the dashboard isn't running, instead of silently failing and deadlocking the next ``map`` call with a progress bar (`#68`_) * Added support for a progress bar without knowing the size of the iterable. It used to disable the progress bar when the size was unknown * Changed how ``max_tasks_active`` is handled. It now applies to the number of tasks that are currently being processed, instead of the number of chunks of tasks, as you would expect from the name. Previously, when the chunk size was set to anything other than 1, the number of active tasks could be higher than ``max_tasks_active`` * Updated some exception messages and docs (`#69`_) * Changed how worker results, restarts, timeouts, unexpected deaths, and exceptions are handled. They are now handled by individual threads such that the main thread is more responsive. The API is the same, so no user changes are needed * Mixing multiple ``map`` calls now raises an error (see :ref:`mixing-multiple-map-calls`) * Fixed a bug where calling a ``map`` function with a progress bar multiple times in a row didn't display the progress bar correctly * Fixed a bug where the dashboard didn't show an error when an exit function raised an exception .. _#63: https://github.com/sybrenjansen/mpire/issues/63 .. _#68: https://github.com/sybrenjansen/mpire/issues/68 .. _#69: https://github.com/sybrenjansen/mpire/issues/69 .. _#71: https://github.com/sybrenjansen/mpire/issues/71 2.6.0 ----- *(2022-08-29)* * Added Python 3.10 support * The ``tqdm`` progress bar can now be customized using the ``progress_bar_options`` parameter in the ``map`` functions (`#57`_) * Using ``progress_bar_position`` from a ``map`` function is now deprecated and will be removed in MPIRE v2.10.0. Use ``progress_bar_options['position']`` instead * Deprecated ``enable_insights`` from a ``map`` function, use ``enable_insights`` in the WorkerPool constructor instead * Fixed a bug where a worker could exit before an exception was entirely sent over the queue, causing a deadlock (`#56`_) * Fixed a bug where exceptions with init arguments weren't handled correctly (`#58`_) * Fixed a rare and weird bug in Windows that could cause a deadlock (probably fixes `#55`_) .. _#55: https://github.com/sybrenjansen/mpire/issues/55 .. _#56: https://github.com/sybrenjansen/mpire/issues/56 .. _#57: https://github.com/sybrenjansen/mpire/issues/57 .. _#58: https://github.com/sybrenjansen/mpire/issues/58 2.5.0 ----- *(2022-07-25)* * Added the option to fix the order of tasks given to the workers (`#46`_) * Fixed a bug where updated WorkerPool parameters aren't used in subsequent ``map`` calls when ``keep_alive`` is enabled .. _#46: https://github.com/sybrenjansen/mpire/issues/46 2.4.0 ----- *(2022-05-25)* * A timeout for the target, ``worker_init``, and ``worker_exit`` functions can be specified after which a worker is stopped (`#36`_) * A WorkerPool can now be started within a thread which isn't the main thread (`#44`_) .. _#36: https://github.com/sybrenjansen/mpire/issues/36 .. _#44: https://github.com/sybrenjansen/mpire/issues/44 2.3.5 ----- *(2022-04-25)* * MPIRE now handles defunct child processes properly, instead of deadlocking (`#34`_) * Added benchmark highlights to README (`#38`_) .. _#34: https://github.com/sybrenjansen/mpire/issues/34 .. _#38: https://github.com/sybrenjansen/mpire/issues/38 2.3.4 ----- *(2022-03-29)* * Platform specific dependencies are now handled using environment markers as defined in PEP-508_ (`#30`_) * Fixes hanging ``WorkerPool`` when using ``worker_lifespan`` and returning results that exceed the pipe capacity (`#32`_) * Fixes insights unit tests that could sometime fail because it was too fast .. _PEP-508: https://www.python.org/dev/peps/pep-0508/#environment-markers .. _#30: https://github.com/sybrenjansen/mpire/issues/30 .. _#32: https://github.com/sybrenjansen/mpire/issues/32 2.3.3 ----- *(2021-11-29)* * Changed progress bar handler process to thread, making it more stable (especially in notebooks) * Changed progress bar tasks completed queue to array, to make it more responsive and faster * Disabled the tqdm monitor thread which, in combination with MPIRE's own tqdm lock, could result in deadlocks 2.3.2 ----- *(2021-11-19)* * Included license file in source distribution (`#25`_) .. _#25: https://github.com/sybrenjansen/mpire/pull/25 2.3.1 ----- *(2021-11-16)* * Made connecting to the tqdm manager more robust (`#23`_) .. _#23: https://github.com/sybrenjansen/mpire/issues/23 2.3.0 ----- *(2021-10-15)* * Fixed progress bar in a particular setting with iPython and django installed (`#13`_) * ``keep_alive`` now works even when the function to be called or any other parameter passed to the ``map`` function is changed (`#15`_) * Moved ``enable_insights`` to the WorkerPool constructor. Using ``enable_insights`` from a ``map`` function is now deprecated and will be removed in MPIRE v2.6.0. * Restructured docs and updated several sections for Windows users. .. _#13: https://github.com/sybrenjansen/mpire/pull/13 .. _#15: https://github.com/sybrenjansen/mpire/issues/15 2.2.1 ----- *(2021-08-31)* * Fixed compatibility with newer tqdm versions (``>= 4.62.2``) (`#11`_) .. _#11: https://github.com/sybrenjansen/mpire/issues/11 2.2.0 ----- *(2021-08-30)* * Added support for Windows (`#6`_, `#7`_). Support has a few caveats: * When using worker insights the arguments of the top 5 longest tasks are not available * Progress bar is not supported when using threading as start method * When using ``dill`` and an exception occurs, or when the exception occurs in an exit function, it can print additional ``OSError`` messages in the terminal, but these can be safely ignored. .. _#6: https://github.com/sybrenjansen/mpire/issues/6 .. _#7: https://github.com/sybrenjansen/mpire/issues/7 2.1.1 ----- *(2021-08-26)* * Fixed a bug with newer versions of tqdm. The progress bar would throw an ``AttributeError`` when connected to a dashboard. * README and documentation updated 2.1.0 ----- *(2021-08-06)* * Workers now have their own task queue, which speeds up tasks with bigger payloads * Fixed progress bar showing error information when completed without error * Fixed progress bar and worker insights not displaying properly when using threading * Progress bar handling improved accross several scenarios * Dashboard can now handle progress bars when using ``spawn`` or ``forkserver`` as start method * Added closing of ``multiprocessing.JoinableQueue`` objects, to clean up intermediate junk * Removed ``numpy`` dependency * Made ``dill`` optional again. In many cases it slows processing down 2.0.0 ----- *(2021-07-07)* * Worker insights added, providing users insight in multiprocessing efficiency * ``worker_init`` and ``worker_exit`` parameters added to each ``map`` function * ``max_active_tasks`` is now set to ``n_jobs * 2`` when ``max_active_tasks=None``, to speed up most jobs * ``n_splits`` is now set to ``n_jobs * 64`` when both ``chunk_size`` and ``n_splits`` are ``None`` * Dashboard ports can now be configured * Renamed ``func_pointer`` to ``func`` in each ``map`` function * Fixed a bug with the `threading` backend not terminating correctly * Fixed a bug with the progress bar not showing correctly in notebooks * Using ``multiprocess`` is now the default * Added some debug logging * Refactored a lot of code * Minor bug fixes, which should make things more stable. * Removed Python 3.5 support * Removed ``add_task``, ``get_result``, ``insert_poison_pill``, ``stop_workers``, and ``join`` functions from :obj:`mpire.WorkerPool`. Made ``start_workers`` private. There wasn't any reason to use these functions. 1.2.2 ----- *(2021-04-23)* * Updated documentation CSS which fixes bullet lists not showing properly 1.2.1 ----- *(2021-04-22)* * Updated some unittests and fixed some linting issues * Minor improvements in documentation 1.2.0 ----- *(2021-04-22)* * Workers can be kept alive in between consecutive map calls * Setting CPU affinity is no longer restricted to Linux platforms * README updated to use RST format for better compatibility with PyPI * Added classifiers to the setup file 1.1.3 ----- *(2020-09-03)* * First public release on Github and PyPi 1.1.2 ----- *(2020-08-27)* * Added missing typing information * Updated some docstrings * Added license 1.1.1 ----- *(2020-02-19)* * Changed ``collections.Iterable`` to ``collections.abc.Iterable`` due to deprecation of the former 1.1.0 ----- *(2019-10-31)* * Removed custom progress bar support to fix Jupyter notebook support * New ``progress_bar_position`` parameter is now available to set the position of the progress bar when using nested worker pools * Screen resizing is now supported when using a progress bar 1.0.0 ----- *(2019-10-29)* * Added the MPIRE dashboard * Added ``threading`` as a possible backend * Progress bar handling now occurs in a separate process, instead of a thread, to improve responsiveness * Refactoring of code and small bug fixes in error handling * Removed deprecated functionality 0.9.0 ----- *(2019-03-11)* * Added support for using different start methods ('spawn' and 'forkserver') instead of only the default method 'fork' * Added optional support for using dill_ in multiprocessing by utilizing the multiprocess_ library * The ``mpire.Worker`` class is no longer directly available .. _dill: https://pypi.org/project/dill/ .. _multiprocess: https://pypi.org/project/multiprocess/ 0.8.1 ----- *(2019-02-06)* * Fixed bug when process would hang when progress bar was set to ``True`` and an empty iterable was provided 0.8.0 ----- *(2018-11-01)* * Added support for worker state * Chunking numpy arrays is now done using numpy slicing * :meth:`mpire.WorkerPool.map` now supports automatic concatenation of numpy array output 0.7.2 ----- *(2018-06-14)* * Small bug fix when not passing on a boolean or ``tqdm`` object for the ``progress_bar`` parameter 0.7.1 ----- *(2017-12-20)* * You can now pass on a dictionary as an argument which will be unpacked accordingly using the ``**``-operator. * New function :meth:`mpire.utils.make_single_arguments` added which allows you to create an iterable of single argument tuples out of an iterable of single arguments 0.7.0 ----- *(2017-12-11)* * :meth:`mpire.utils.chunk_tasks` is now available as a public function * Chunking in above function and map functions now accept a ``n_splits`` parameter * ``iterable_of_args`` in map functions can now contain single values instead of only iterables * ``tqdm`` is now available from the MPIRE package which automatically switches to the Jupyter/IPython notebook widget when available * Small bugfix in cleaning up a worker pool when no map function was called 0.6.2 ----- *(2017-11-07)* * Fixed a second bug where the main process could get unresponsive when an exception was raised 0.6.1 ----- *(2017-11-06)* * Fixed bug where sometimes exceptions fail to pickle * Fixed a bug where the main process could get unresponsive when an exception was raised * Child processes are now cleaned up in parallel when an exception was raised 0.6.0 ----- *(2017-11-03)* * ``restart_workers`` parameter is now deprecated and will be removed from v1.0.0 * Progress bar functionality added (using tqdm_) * Improved error handling in user provided functions * Fixed randomly occurring ``BrokenPipeErrors`` and deadlocks 0.5.1 ----- *(2017-10-12)* * Child processes can now also be pinned to a range of CPUs, instead of only a single one. You can also specify a single CPU or range of CPUs that have to be shared between all child processes 0.5.0 ----- *(2017-10-06)* * Added CPU pinning. * Default number of processes to spawn when using ``n_jobs=None`` is now set to the number of CPUs available, instead of ``cpu_count() - 1`` 0.4.0 ----- *(2017-10-05)* * Workers can now be started as normal child processes (non-deamon) such that nested :obj:`mpire.WorkerPool` s are possible 0.3.0 ----- *(2017-09-15)* * The worker ID can now be passed on the function to be executed by using the :meth:`mpire.WorkerPool.pass_on_worker_id` function * Removed the use of ``has_return_value_with_shared_objects`` when using :meth:`mpire.WorkerPool.set_shared_objects`. MPIRE now handles both cases out of the box 0.2.0 ----- *(2017-06-27)* * Added docs 0.1.0 ----- First release .. _tqdm: https://pypi.python.org/pypi/tqdm mpire-2.10.2/docs/conf.py000066400000000000000000000401251461637447300151600ustar00rootroot00000000000000#!/usr/bin/env python3 # # mpire documentation build configuration file, created by # sphinx-quickstart on Tue Jun 27 15:35:20 2017. # # 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. # 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. from datetime import datetime import re _version = '2.10.2' def isBoostFunc(what, obj): return what == 'function' and obj.__repr__().startswith(' 0 and l2[0] == "": l2 = l2[1:] # We must replace lines one by one (in-place) :-| (i.e., we cannot set lines = l2) # Knowing that l2 is always shorter than lines (l2 is docstring with the signature stripped off) for i in range(0, len(lines)): lines[i] = l2[i] if i < len(l2) else '' def fixSignature(app, what, name, obj, options, signature, return_annotation): if what in ('attribute', 'class'): return signature, None if isBoostFunc(what, obj) or isBoostMethod(what, obj) or isBoostStaticMethod(what, obj): # Format. Note that second argument has to be None return boostFuncSignature(name, obj)[0], None def replaceObjSelf(sig): # Split argument list only once on the comma. In the case that '(Obj)self' exists in the parameter list the first # part includes the '(Obj)self' part, the second part includes the remaining parameters. Due to optional parameters # there could be a '[' token in the '(Obj)self' part, which should be retained. Note that we only want to remove # the '(Obj)' part. try: ss = sig.split(',', 1) if ss[0].endswith('self') or ss[0].endswith('self ['): if ss[0].endswith('['): sig = '[' + ss[1] elif len(ss) > 1: sig = ss[1] else: sig = "" elif ' -> ' in ss[0]: sig = ') -> ' + ss[0].split(' -> ')[1] except IndexError: # grab the return value try: sig = ') -> ' + sig.split('->')[-1] except IndexError: sig = ')' return '(' + sig def boostFuncSignature(name, obj): """ Scan docstring of obj, returning tuple of properly formatted boost python signature (first line of the docstring) and the rest of docstring (as list of lines). The rest of docstring is stripped of 4 leading spaces which are automatically added by boost. """ # Obtain the doc of this function, if it is None it is not a Boost method doc = obj.__doc__ if doc is None: return None, None # Obtain the class name and function name cname = name.split('.')[1] nname = name.split('.')[-1] # The first line of the doc is always empty, so we remove it docc = doc.split('\n') if len(docc) < 2: return None, docc doc1 = docc[1] # Functions with weird docstring, likely not documented by boost if not re.match('^' + nname + r'(.*)->.*$', doc1): return None, docc # If doc1 ends with a ':' it means that there is a function description doc string following in the next part if doc1.endswith(':'): doc1 = doc1[:-1] strippedDoc = doc.split('\n')[2:] strippedDoc = [line.replace("->", "→") for line in strippedDoc] # Replace '(Obj)self' with 'self' sig = doc1.split('(', 1)[1] sig = replaceObjSelf(sig) # Fix doc string when overloaded functions are present. When there are overloaded function without description those # functions will not be displayed correctly, so we add a custom description to those new_strippedDoc = [] has_description = False for line_nr, line in enumerate(strippedDoc): if line.startswith(nname): line = replaceObjSelf(line) new_strippedDoc.append(nname + line) if not has_description: new_strippedDoc.append(" Overloaded function without description") else: if len(line): has_description = True new_strippedDoc.append(line) # Fix signature of classes exposed with the vector_indexing_suite (the classes are named [...]Vector) if cname.endswith("Vector") and len(cname) > len("Vector"): if nname == "__contains__": sig = "( self, (object)arg) -> bool" if nname == "__delitem__": sig = "( self, (int)idx) -> None" if nname == "__getitem__": sig = "( self, (int)idx) -> object" if nname == "__setitem__": sig = "( self, (int)idx, (object)value) -> None" if nname == "append": sig = "( self, (object)value) -> None" if nname == "extend": sig = "( self, (object)list_of_values) -> None" # Fix signature of constructors, they do not return None if nname == "__init__": if sig.endswith("None"): sig = sig[:-4] + cname elif sig.endswith("None "): sig = sig[:-5] + cname new_strippedDoc = [line.replace("→ None", "→ %s" % cname) for line in new_strippedDoc] return sig, new_strippedDoc def skipMember(app, what, name, obj, skip, options): # Skip some special methods if name.endswith('__getinitargs__') \ or name.endswith('__instance_size__') \ or name.endswith('__module__') \ or name.endswith('__reduce__') \ or name.endswith('__safe_for_unpickling__') \ or name.endswith('__slots__') \ or name.endswith('_enum.names') \ or name.endswith('_enum.values') \ or name.endswith('__getstate__') \ or name.endswith('__setstate__') \ or name.endswith('__getstate_manages_dict__') \ or name.endswith('__doc__'): return True # Skip some enum class members if isinstance(obj, dict) and name in {"names", "values"}: return True return skip def setup(app): # Register custom apps app.connect('autodoc-process-docstring', fixDocstring) app.connect('autodoc-process-signature', fixSignature) app.connect('autodoc-skip-member', skipMember) app.add_stylesheet('css/custom.css') # -- 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.todo', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', 'sphinx_autodoc_typehints', 'sphinxcontrib.images' ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'mpire' copyright = '%d, Sybren Jansen' % datetime.now().year author = 'Sybren Jansen' # 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 = _version # The full version, including alpha/beta/rc tags. release = _version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. 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. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # 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 = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # # html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme' # 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. # " v documentation" by default. # # html_title = 'mpire v0.2.0' # 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 = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # 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 # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'mpiredoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'mpire.tex', 'mpire Documentation', 'Sybren Jansen', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # 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 # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # 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 = [ (master_doc, 'mpire', 'mpire Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'mpire', 'mpire Documentation', author, 'mpire', 'A Python package for easy multiprocessing, but faster than multiprocessing.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False mpire-2.10.2/docs/contributing.rst000066400000000000000000000027551461637447300171310ustar00rootroot00000000000000Contribution guidelines ======================= If you want to contribute to MPIRE, great! Please follow the steps below to ensure a smooth process: 1. Clone the project. 2. Create a new branch for your feature or bug fix. Give you branch a meaningful name. 3. Make your feature addition or bug fix. 4. Add tests for it and test it yourself. Make sure it both works for Unix and Windows based systems, or make sure to document why it doesn't work for one of the platforms. 5. Add documentation for it. Don't forget about the changelog: - Reference the issue number from GitHub in the changelog, if applicable (see current changelog for examples). - Don't mention a date or a version number here, but use ``Unreleased`` instead. 6. Commit with a meaningful commit message (e.g. the changelog). 7. Open a pull request. 8. Resolve any issues or comments by the reviewer. 9. Merge PR by squashing all your individual commits. Making a release ---------------- A release is only made by the project maintainer. The following steps are required: 1. Update the changelog with the release date and version number. Version numbers follow the `Semantic Versioning`_ guidelines 2. Update the version number in ``setup.py`` and ``docs/conf.py``. 3. Commit and push the changes. 4. Make sure the tests pass on GitHub Actions. 5. Create a tag for the release by using ``git tag -a vX.Y.Z -m "vX.Y.Z"``. 6. Push the tag to GitHub by using ``git push origin vX.Y.Z``. .. _Semantic Versioning: https://semver.org/ mpire-2.10.2/docs/getting_started.rst000066400000000000000000000120621461637447300176010ustar00rootroot00000000000000Getting started =============== Suppose you have a time consuming function that receives some input and returns its results. This could look like the following: .. code-block:: python import time def time_consuming_function(x): time.sleep(1) # Simulate that this function takes long to complete return ... results = [time_consuming_function(x) for x in range(10)] Running this function takes about 10 seconds to complete. Functions like these are known as `embarrassingly parallel`_ problems, functions that require little to no effort to turn into a parallel task. Parallelizing a simple function as this can be as easy as importing ``multiprocessing`` and using the ``multiprocessing.Pool`` class: .. _embarrassingly parallel: https://en.wikipedia.org/wiki/Embarrassingly_parallel .. code-block:: python from multiprocessing import Pool with Pool(processes=5) as pool: results = pool.map(time_consuming_function, range(10)) We configured to have 5 workers, so we can handle 5 tasks in parallel. As a result, this function will complete in about 2 seconds. MPIRE can be used almost as a drop-in replacement to ``multiprocessing``. We use the :obj:`mpire.WorkerPool` class and call one of the available ``map`` functions: .. code-block:: python from mpire import WorkerPool with WorkerPool(n_jobs=5) as pool: results = pool.map(time_consuming_function, range(10)) Similarly, this will complete in about 2 seconds. The differences in code are small: there's no need to learn a completely new multiprocessing syntax, if you're used to vanilla ``multiprocessing``. The additional available functionality, though, is what sets MPIRE apart. Progress bar ------------ Suppose we want to know the status of the current task: how many tasks are completed, how long before the work is ready? It's as simple as setting the ``progress_bar`` parameter to ``True``: .. code-block:: python with WorkerPool(n_jobs=5) as pool: results = pool.map(time_consuming_function, range(10), progress_bar=True) And it will output a nicely formatted tqdm_ progress bar. MPIRE also offers a dashboard, for which you need to install additional :ref:`dependencies `. See :ref:`Dashboard` for more information. .. _tqdm: https://tqdm.github.io/ Shared objects -------------- If you have one or more objects that you want to share between all workers you can make use of the copy-on-write ``shared_objects`` option of MPIRE. MPIRE will pass on these objects only once for each worker without copying/serialization. Only when the object is altered in the worker function it will start copying it for that worker. .. note:: Copy-on-write is not available on Windows, as it requires the start method ``fork``. .. code-block:: python def time_consuming_function(some_object, x): time.sleep(1) # Simulate that this function takes long to complete return ... def main(): some_object = ... with WorkerPool(n_jobs=5, shared_objects=some_object, start_method='fork') as pool: results = pool.map(time_consuming_function, range(10), progress_bar=True) See :ref:`shared_objects` for more details. Worker initialization --------------------- Need to initialize each worker before starting the work? Have a look at the ``worker_state`` and ``worker_init`` functionality: .. code-block:: python def init(worker_state): # Load a big dataset or model and store it in a worker specific worker_state worker_state['dataset'] = ... worker_state['model'] = ... def task(worker_state, idx): # Let the model predict a specific instance of the dataset return worker_state['model'].predict(worker_state['dataset'][idx]) with WorkerPool(n_jobs=5, use_worker_state=True) as pool: results = pool.map(task, range(10), worker_init=init) Similarly, you can use the ``worker_exit`` parameter to let MPIRE call a function whenever a worker terminates. You can even let this exit function return results, which can be obtained later on. See the :ref:`worker_init_exit` section for more information. Worker insights --------------- When your multiprocessing setup isn't performing as you want it to and you have no clue what's causing it, there's the worker insights functionality. This will give you some insight in your setup, but it will not profile the function you're running (there are other libraries for that). Instead, it profiles the worker start up time, waiting time and working time. When worker init and exit functions are provided it will time those as well. Perhaps you're sending a lot of data over the task queue, which makes the waiting time go up. Whatever the case, you can enable and grab the insights using the ``enable_insights`` flag and :meth:`mpire.WorkerPool.get_insights` function, respectively: .. code-block:: python with WorkerPool(n_jobs=5, enable_insights=True) as pool: results = pool.map(time_consuming_function, range(10)) insights = pool.get_insights() See :ref:`worker insights` for a more detailed example and expected output. mpire-2.10.2/docs/index.rst000066400000000000000000000053271461637447300155270ustar00rootroot00000000000000Welcome to the MPIRE documentation! =================================== MPIRE, short for MultiProcessing Is Really Easy, is a Python package for multiprocessing. MPIRE is faster in most scenarios, packs more features, and is generally more user-friendly than the default multiprocessing package. It combines the convenient map like functions of ``multiprocessing.Pool`` with the benefits of using copy-on-write shared objects of ``multiprocessing.Process``, together with easy-to-use worker state, worker insights, worker init and exit functions, timeouts, and progress bar functionality. Features -------- - Faster execution than other multiprocessing libraries. See benchmarks_. - Intuitive, Pythonic syntax - Multiprocessing with ``map``/``map_unordered``/``imap``/``imap_unordered``/``apply``/``apply_async`` functions - Easy use of copy-on-write shared objects with a pool of workers (copy-on-write is only available for start method ``fork``, so it's not supported on Windows) - Each worker can have its own state and with convenient worker init and exit functionality this state can be easily manipulated (e.g., to load a memory-intensive model only once for each worker without the need of sending it through a queue) - Progress bar support using tqdm_ (``rich`` and notebook widgets are supported) - Progress dashboard support - Worker insights to provide insight into your multiprocessing efficiency - Graceful and user-friendly exception handling - Timeouts, including for worker init and exit functions - Automatic task chunking for all available map functions to speed up processing of small task queues (including numpy arrays) - Adjustable maximum number of active tasks to avoid memory problems - Automatic restarting of workers after a specified number of tasks to reduce memory footprint - Nested pool of workers are allowed when setting the ``daemon`` option - Child processes can be pinned to specific or a range of CPUs - Optionally utilizes dill_ as serialization backend through multiprocess_, enabling parallelizing more exotic objects, lambdas, and functions in iPython and Jupyter notebooks. MPIRE has been tested on Linux, macOS, and Windows. There are a few minor known caveats for Windows and macOS users, which can be found at :ref:`troubleshooting_windows`. .. _benchmarks: https://towardsdatascience.com/mpire-for-python-multiprocessing-is-really-easy-d2ae7999a3e9 .. _dill: https://pypi.org/project/dill/ .. _multiprocess: https://github.com/uqfoundation/multiprocess .. _tqdm: https://tqdm.github.io/ Contents -------- .. toctree:: :hidden: self .. toctree:: :maxdepth: 3 :titlesonly: install getting_started usage/index troubleshooting reference/index contributing changelog mpire-2.10.2/docs/install.rst000066400000000000000000000050261461637447300160620ustar00rootroot00000000000000Installation ============ :ref:`MPIRE ` builds are distributed through PyPi_. .. _PyPi: https://pypi.org/ MPIRE can be installed through pip: .. code-block:: bash pip install mpire and is available through conda-forge: .. code-block:: bash conda install -c conda-forge mpire Dependencies ------------ - Python >= 3.8 Python packages (installed automatically when installing MPIRE): - tqdm - pygments - pywin32 (Windows only) - importlib_resources (Python < 3.9 only) .. note:: When using MPIRE on Windows with conda, you might need to install ``pywin32`` using ``conda install pywin32`` when encountering a ``DLL failed to load`` error. .. _dilldep: Dill ~~~~ For some functions or tasks it can be useful to not rely on pickle, but on some more powerful serialization backend, like dill_. ``dill`` isn't installed by default as it has a BSD license, while MPIRE has an MIT license. If you want to use it, the license of MPIRE will change to a BSD license as well, as required by the original BSD license. See the `BSD license of multiprocess`_ for more information. You can enable ``dill`` by executing: .. code-block:: bash pip install mpire[dill] This will install multiprocess_, which uses ``dill`` under the hood. You can enable the use of ``dill`` by setting ``use_dill=True`` in the :obj:`mpire.WorkerPool` constructor. .. _dill: https://pypi.org/project/dill/ .. _multiprocess: https://github.com/uqfoundation/multiprocess .. _BSD license of multiprocess: https://github.com/uqfoundation/multiprocess/blob/master/LICENSE .. _richdep: Rich progress bars ~~~~~~~~~~~~~~~~~~ If you want to use rich_ progress bars, you have to install the dependencies for it manually: .. code-block:: bash pip install rich .. _rich: https://github.com/Textualize/rich .. _dashboarddep: Dashboard ~~~~~~~~~ Optionally, you can install the dependencies for the MPIRE dashboard, which depends on Flask_. Similarly as with ``dill``, ``Flask`` has a BSD-license. Installing these dependencies will change the license of MPIRE to BSD as well. See the `BSD license of Flask`_ for more information. The dashboard allows you to see progress information from a browser. This is convenient when running scripts in a notebook or screen, or want to share the progress information with others. Install the appropriate dependencies to enable this: .. code-block:: bash pip install mpire[dashboard] .. _Flask: https://flask.palletsprojects.com/en/1.1.x/ .. _BSD license of Flask: https://github.com/pallets/flask/blob/main/LICENSE.rst mpire-2.10.2/docs/mpire.rst000066400000000000000000000162031461637447300155270ustar00rootroot00000000000000:orphan: .. _secret: "The Empire" ============ .. code-block:: none ,ooo888888888888888oooo, o8888YYYYYY77iiiiooo8888888o 8888YYYY77iiYY8888888888888888 [88YYY77iiY88888888888888888888] 88YY7iYY888888888888888888888888 [88YYi 88888888888888888888888888] i88Yo8888888888888888888888888888i i] ^^^88888888^^^ o [i oi8 i o8o i 8io ,77788o ^^ ,oooo8888888ooo, ^ o88777, 7777788888888888888888888888888888877777 77777888888888888888888888888888877777 77777788888888^7777777^8888888777777 ,oooo888 ooo 88888778888^7777ooooo7777^8887788888 ,o88^^^^888oo o8888777788[];78 88888888888888888888888888888888888887 7;8^ 888888888oo^88 o888888iii788 ]; o 78888887788788888^;;^888878877888887 o7;[]88888888888888o 88888877 ii78[]8;7o 7888878^ ^8788^;;;;;;^878^ ^878877 o7;8 ]878888888888888 [88888888887888 87;7oo 777888o8888^;ii;;ii;^888o87777 oo7;7[]8778888888888888 88888888888888[]87;777oooooooooooooo888888oooooooooooo77;78]88877i78888888888 o88888888888888 877;7877788777iiiiiii;;;;;iiiiiiiii77877i;78] 88877i;788888888 88^;iiii^88888 o87;78888888888888888888888888888888888887;778] 88877ii;7788888 ;;;iiiii7iiii^ 87;;888888888888888888888888888888888888887;778] 888777ii;78888 ;iiiii7iiiii7iiii77;i88888888888888888888i7888888888888888877;77i 888877777ii78 iiiiiiiiiii7iiii7iii;;;i7778888888888888ii7788888888888777i;;;;iiii 88888888888 i;iiiiiiiiiiii7iiiiiiiiiiiiiiiiiiiiiiiiii8877iiiiiiiiiiiiiiiiiii877 88888 ii;;iiiiiiiiiiiiii;;;ii^^^;;;ii77777788888888888887777iii;; 77777 78 77iii;;iiiiiiiiii;;;ii;;;;;;;;;^^^^8888888888888888888777ii;; ii7 ;i78 ^ii;8iiiiiiii ';;;;ii;;;;;;;;;;;;;;;;;;^^oo ooooo^^^88888888;;i7 7;788 o ^;;^^88888^ 'i;;;;;;;;;;;;;;;;;;;;;;;;;;;^^^88oo^^^^888ii7 7;i788 88ooooooooo ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 788oo^;; 7;i888 887ii8788888 ;;;;;;;ii;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;^87 7;788 887i8788888^ ;;;;;;;ii;;;;;;;oo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,, ;;888 87787888888 ;;;;;;;ii;;;;;;;888888oo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,;i788 87i8788888^ ';;;ii;;;;;;;8888878777ii8ooo;;;;;;;;;;;;;;;;;;;;;;;;;;i788 7 77i8788888 ioo;;;;;;oo^^ooooo ^7i88^ooooo;;;;;;;;;;;;;;;;;;;;i7888 78 7i87788888o 7;ii788887i7;7;788888ooooo7888888ooo;;;;;;;;;;;;;;oo ^^^ 78 i; 7888888^ 8888^o;ii778877;7;7888887;;7;7788878;878;; ;;;;;;;i78888o ^ i8 788888 [88888^^ ooo ^^^^^;;77888^^^^;;7787^^^^ ^^;;;; iiii;i78888888 ^8 7888^ [87888 87 ^877i;i8ooooooo8778oooooo888877ii; iiiiiiii788888888 ^^^ [7i888 87;; ^8i;;i7888888888888888887888888 i7iiiiiii88888^^ 87;88 o87;;;;o 87i;;;78888788888888888888^^ o 8ii7iiiiii;; 87;i8 877;77888o ^877;;;i7888888888888^^ 7888 78iii7iii7iiii ^87; 877;778888887o 877;;88888888888^ 7ii7888 788oiiiiiiiii ^ 877;7 7888888887 877i;;8888887ii 87i78888 7888888888 [87;;7 78888888887 87i;;888887i 87ii78888 7888888888] 877;7 7788888888887 887i;887i^ 87ii788888 78888888888 87;i8 788888888888887 887ii;;^ 87ii7888888 78888888888 [87;i8 7888888888888887 ^^^^ 87ii77888888 78888888888 87;;78 7888888888888887ii 87i78888888 778888888888 87;788 7888888888888887i] 87i78888888 788888888888 [87;88 778888888888888887 7ii78888888 788888888888 87;;88 78888888888888887] ii778888888 78888888888] 7;;788 7888888888888888] i7888888888 78888888888' 7;;788 7888888888888888 'i788888888 78888888888 7;i788 788888888888888] 788888888 77888888888] '7;788 778888888888888] [788888888 78888888888' ';77888 78888888888888 8888888888 7888888888] 778888 78888888888888 8888888888 7888888888] 78888 7888888888888] [8888888888 7888888888 7888 788888888888] 88888888888 788888888] 778 78888888888] ]888888888 778888888] oooooo ^88888^ ^88888^^^^^^^^8888] 87;78888ooooooo8o ,oooooo oo888oooooo [877;i77888888888] [;78887i8888878i7888; ^877;;ii7888ii788 ;i777;7788887787;778; ^87777;;;iiii777 ;77^^^^^^^^^^^^^^^^;; ^^^^^^^^^ii7] ^ o88888888877iiioo 77777o [88777777iiiiii;;778 77777iii 8877iiiii;;;77888888] 77iiii;8 [77ii;778 788888888888 7iii;;88 iii;78888 778888888888 77i;78888] ;;;;i88888 78888888888 ,7;78888888 [;;i788888 7888888888] i;788888888 ;i7888888 7888888888 ;788888888] i77888888 788888888] ';88888888' [77888888 788888888] [[8ooo88] 78888888 788888888 [88888] 78888888 788888888 ^^^ [7888888 77888888] 88888888 7888887 77888888 7888887 ;i88888 788888i ,;;78888 788877i7 ,7;;i;777777i7i;;7 87778^^^ ^^^^87778 ^^^^ o777777o ^^^ o77777iiiiii7777o 7777iiii88888iii777 ;;;i7778888888877ii;; [i77888888^^^^8888877i] 77888^oooo8888oooo^8887] [788888888888888888888888] 88888888888888888888888888 ]8888888^iiiiiiiii^888888] iiiiiiiiiiiiiiiiiiiiii ^^^^^^^^^^^^^mpire-2.10.2/docs/reference/000077500000000000000000000000001461637447300156155ustar00rootroot00000000000000mpire-2.10.2/docs/reference/index.rst000066400000000000000000000014131461637447300174550ustar00rootroot00000000000000API Reference ============= .. contents:: Contents :local: WorkerPool ---------- .. autoclass:: mpire.WorkerPool :members: :special-members: AsyncResult ----------- .. autoclass:: mpire.async_result.AsyncResult :members: :special-members: Task chunking ------------- .. autofunction:: mpire.utils.chunk_tasks Converting iterable of arguments -------------------------------- .. autofunction:: mpire.utils.make_single_arguments Dashboard --------- .. autofunction:: mpire.dashboard.start_dashboard .. autofunction:: mpire.dashboard.connect_to_dashboard .. autofunction:: mpire.dashboard.shutdown_dashboard .. autofunction:: mpire.dashboard.get_stacklevel .. autofunction:: mpire.dashboard.set_stacklevel Other ----- .. autofunction:: mpire.cpu_count mpire-2.10.2/docs/troubleshooting.rst000066400000000000000000000162131461637447300176430ustar00rootroot00000000000000Troubleshooting =============== This section describes some known problems that can arise when using MPIRE. .. contents:: Contents :depth: 2 :local: .. _troubleshooting_progress_bar: Progress bar issues with Jupyter notebooks ------------------------------------------ When using the progress bar in a Jupyter notebook you might encounter some issues. A few of these are described below, together with possible solutions. IProgress not found ~~~~~~~~~~~~~~~~~~~ When you something like ``ImportError: IProgress not found. Please update jupyter and ipywidgets.``, this means ``ipywidgets`` is not installed. You can install it using ``pip``: .. code-block:: bash pip install ipywidgets or conda: .. code-block:: bash conda install -c conda-forge ipywidgets Have a look at the `ipywidgets documentation`_ for more information. .. _ipywidgets documentation: https://ipywidgets.readthedocs.io/en/stable/user_install.html Widget Javascript not detected ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you see something like ``Widget Javascript not detected. It may not be enabled properly.``, this means the Javascript extension is not enabled. You can enable it using the following command before starting your notebook: .. code-block:: bash jupyter nbextension enable --py --sys-prefix widgetsnbextension Note that you have to restart your notebook server after enabling the extension, simply restarting the kernel won't be enough. Unit tests ---------- When using the ``'spawn'`` or ``'forkserver'`` method you'll probably run into one or two issues when running unittests in your own package. One problem that might occur is that your unittests will restart whenever the piece of code containing such a start method is called, leading to very funky terminal output. To remedy this problem make sure your ``setup`` call in ``setup.py`` is surrounded by an ``if __name__ == '__main__':`` clause: .. code-block:: python from setuptools import setup if __name__ == '__main__': # Call setup and install any dependencies you have inside the if-clause setup(...) See the 'Safe importing of main module' section at caveats_. The second problem you might encounter is that the semaphore tracker of multiprocessing will complain when you run individual (or a selection of) unittests using ``python setup.py test -s tests.some_test``. At the end of the tests you will see errors like: .. code-block:: python Traceback (most recent call last): File ".../site-packages/multiprocess/semaphore_tracker.py", line 132, in main cache.remove(name) KeyError: b'/mp-d3i13qd5' .../site-packages/multiprocess/semaphore_tracker.py:146: UserWarning: semaphore_tracker: There appear to be 58 leaked semaphores to clean up at shutdown len(cache)) .../site-packages/multiprocess/semaphore_tracker.py:158: UserWarning: semaphore_tracker: '/mp-f45dt4d6': [Errno 2] No such file or directory warnings.warn('semaphore_tracker: %r: %s' % (name, e)) ... Your unittests will still succeed and run OK. Unfortunately, I've not found a remedy to this problem using ``python setup.py test`` yet. What you can use instead is something like the following: .. code-block:: python python -m unittest tests.some_test This will work just fine. See the unittest_ documentation for more information. .. _caveats: https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods .. _unittest: https://docs.python.org/3.4/library/unittest.html#command-line-interface Shutting down takes a long time on error ---------------------------------------- When you issue a ``KeyboardInterrupt`` or when an error occured in the function that's run in parallel, there are situations where MPIRE needs a few seconds to gracefully shutdown. This has to do with the fact that in these situations the task or results queue can be quite full, still. MPIRE drains these queues until they're completely empty, as to properly shutdown and clean up every communication channel. To remedy this issue you can use the ``max_tasks_active`` parameter and set it to ``n_jobs * 2``, or similar. Aside from the added benefit that the workers can start more quickly, the queues won't get that full anymore and shutting down will be much quicker. See :ref:`max_active_tasks` for more information. When you're using a lazy map function also be sure to iterate through the results, otherwise that queue will be full and draining it will take a longer time. .. _unpickable_tasks: Unpicklable tasks/results ------------------------- Sometimes you can encounter deadlocks in your code when using MPIRE. When you encounter this, chances are some tasks or results from your script can't be pickled. MPIRE makes use of multiprocessing queues for inter-process communication and if your function returns unpicklable results the queue will unfortunately deadlock. The only way to remedy this problem in MPIRE would be to manually pickle objects before sending it to a queue and quit gracefully when encountering a pickle error. However, this would mean objects would always be pickled twice. This would add a heavy performance penalty and is therefore not an acceptable solution. Instead, the user should make sure their tasks and results are always picklable (which in most cases won't be a problem), or resort to setting ``use_dill=True``. The latter is capable of pickling a lot more exotic types. See :ref:`use_dill` for more information. AttributeError: Can't get attribute '' on --------------------------------------------------------------------------------------- This error can occur when inside an iPython or Jupyter notebook session and the function to parallelize is defined in that session. This is often the result of using ``spawn`` as start method (the default on Windows), which starts a new process without copying the function in question. This error is actually related to the :ref:`unpickable_tasks` problem and can be solved in a similar way. I.e., you can define your function in a file that can be imported by the child process, or you can resort to using ``dill`` by setting ``use_dill=True``. See :ref:`use_dill` for more information. .. _troubleshooting_windows: Windows ------- * When using ``dill`` and an exception occurs, or when the exception occurs in an exit function, it can print additional ``OSError`` messages in the terminal, but they can be safely ignored. * The ``mpire-dashboard`` script does not work on Windows. .. _troubleshooting_macos: macOS ----- * When encountering ``OSError: [Errno 24] Too many open files`` errors, use ``ulimit -n `` to increase the limit of the number of open files. This is required because MPIRE uses file-descriptor based synchronization primitives and macOS has a very low default limit. For example, MPIRE uses about 190 file descriptors when using 10 workers. * Pinning of processes to CPU cores is not supported on macOS. This is because macOS does not support the ``sched_setaffinity`` system call. A warning will be printed when trying to use this feature. mpire-2.10.2/docs/usage/000077500000000000000000000000001461637447300147635ustar00rootroot00000000000000mpire-2.10.2/docs/usage/apply.rst000066400000000000000000000140041461637447300166410ustar00rootroot00000000000000.. _apply-family: Apply family ============ .. contents:: Contents :depth: 2 :local: :obj:`mpire.WorkerPool` implements two ``apply`` functions, which are very similar to the ones in the :mod:`multiprocessing` module: :meth:`mpire.WorkerPool.apply` Apply a function to a single task. This is a blocking call. :meth:`mpire.WorkerPool.apply_async` A variant of the above, but which is non-blocking. This returns an :obj:`mpire.async_result.AsyncResult` object. ``apply`` --------- The ``apply`` function is a blocking call, which means that it will not return until the task is completed. If you want to run multiple different tasks in parallel, you should use the ``apply_async`` function instead. If you require to run the same function for many tasks in parallel, use the ``map`` functions instead. The ``apply`` function takes a function, positional arguments, and keyword arguments, similar to how :mod:`multiprocessing` does it. .. code-block:: python def task(a, b, c, d): return a + b + c + d with WorkerPool(n_jobs=1) as pool: result = pool.apply(task, args=(1, 2), kwargs={'d': 4, 'c': 3}) print(result) ``apply_async`` --------------- The ``apply_async`` function is a non-blocking call, which means that it will return immediately. It returns an :obj:`mpire.async_result.AsyncResult` object, which can be used to get the result of the task at a later moment in time. The ``apply_async`` function takes the same parameters as the ``apply`` function. .. code-block:: python def task(a, b): return a + b with WorkerPool(n_jobs=4) as pool: async_results = [pool.apply_async(task, args=(i, i)) for i in range(10)] results = [async_result.get() for async_result in async_results] Obtaining the results should happen while the pool is still running! E.g., the following will deadlock: .. code-block:: with WorkerPool(n_jobs=4) as pool: async_results = [pool.apply_async(task, args=(i, i)) for i in range(10)] # Will wait forever results = [async_result.get() for async_result in async_results] You can, however, make use of the :meth:`mpire.WorkerPool.stop_and_join()` function to stop the workers and join the pool. This will make sure that all tasks are completed before the pool exits. .. code-block:: with WorkerPool(n_jobs=4) as pool: async_results = [pool.apply_async(task, args=(i, i)) for i in range(10)] pool.stop_and_join() # Will not deadlock results = [async_result.get() for async_result in async_results] AsyncResult ----------- The :obj:`mpire.async_result.AsyncResult` object has the following convenient methods: .. code-block:: python with WorkerPool(n_jobs=1) as pool: async_result = pool.apply_async(task, args=(1, 1)) # Check if the task is completed is_completed = async_result.ready() # Wait until the task is completed, or until the timeout is reached. async_result.wait(timeout=10) # Get the result of the task. This will block until the task is completed, # or until the timeout is reached. result = async_result.get(timeout=None) # Check if the task was successful (i.e., did not raise an exception). # This will raise an exception if the task is not completed yet. is_successful = async_result.successful() Callbacks --------- Each ``apply`` function has a ``callback`` and ``error_callback`` argument. These are functions which are called when the task is finished. The ``callback`` function is called with the result of the task when the task was completed successfully, and the ``error_callback`` is called with the exception when the task failed. .. code-block:: python def task(a): return a + 1 def callback(result): print("Task completed successfully with result:", result) def error_callback(exception): print("Task failed with exception:", exception) with WorkerPool(n_jobs=1) as pool: pool.apply(task, 42, callback=callback, error_callback=error_callback) Worker init and exit -------------------- As with the ``map`` family of functions, the ``apply`` family of functions also has ``worker_init`` and ``worker_exit`` arguments. These are functions which are called when a worker is started and stopped, respectively. See :ref:`worker_init_exit` for more information on these functions. .. code-block:: python def worker_init(): print("Worker started") def worker_exit(): print("Worker stopped") with WorkerPool(n_jobs=5) as pool: pool.apply(task, 42, worker_init=worker_init, worker_exit=worker_exit) There's a caveat though. When the first ``apply`` or ``apply_async`` function is executed, the entire pool of workers is started. This means that in the above example all five workers are started, while only one was needed. This also means that the ``worker_init`` function is set for all those workers at once. This means you cannot have a different ``worker_init`` function for each apply task. A second, different ``worker_init`` function will simply be ignored. Similarly, the ``worker_exit`` function can only be set once as well. Additionally, exit functions are only called when a worker exits, which in this case translates to when the pool exits. This means that if you call ``apply`` or ``apply_async`` multiple times, the ``worker_exit`` function is only called once at the end. Use :meth:`mpire.WorkerPool.stop_and_join()` to stop the workers, which will cause the ``worker_exit`` function to be triggered for each worker. Timeouts -------- The ``apply`` family of functions also has ``task_timeout``, ``worker_init_timeout`` and ``worker_exit_timeout`` arguments. These are timeouts for the task, the ``worker_init`` function and the ``worker_exit`` function, respectively. They work similarly as those for the ``map`` functions. When a single task times out, only that task is cancelled. The other tasks will continue to run. When a worker init or exit times out, the entire pool is stopped. See :ref:`timeouts` for more information. mpire-2.10.2/docs/usage/dashboard.rst000066400000000000000000000143161461637447300174510ustar00rootroot00000000000000.. _Dashboard: Dashboard ========= The dashboard allows you to see progress information from a browser. This is convenient when running scripts in a notebook or screen, if you want to share the progress information with others, or if you want to get real-time worker insight information. The dashboard dependencies are not installed by default. See :ref:`dashboarddep` for more information. .. contents:: Contents :depth: 2 :local: Starting the dashboard ---------------------- You can start the dashboard programmatically: .. code-block:: python from mpire.dashboard import start_dashboard # Will return a dictionary with dashboard details dashboard_details = start_dashboard() print(dashboard_details) which will print: .. code-block:: python {'dashboard_port_nr': 8080, 'manager_host': 'localhost', 'manager_port_nr': 8081} This will start a dashboard on your local machine on port 8080. When the port is already in use MPIRE will try the next, until it finds an unused one. In the rare case that no ports are available up to port 8099 the function will raise an ``OSError``. By default, MPIRE tries ports 8080-8100. You can override this range by passing on a custom range object: .. code-block:: python dashboard_details = start_dashboard(range(9000, 9100)) The returned dictionary contains the port number that is ultimately chosen. It also contains information on how to connect to this dashboard remotely. Another way of starting a dashboard is by using the bash script (this doesn't work on Windows!): .. code-block:: bash $ mpire-dashboard This will start a dashboard with the connection details printed on screen. It will say something like: .. code-block:: bash Starting MPIRE dashboard... MPIRE dashboard started on http://localhost:8080 Server is listening on localhost:8098 -------------------------------------------------- The server part corresponds to the ``manager_host`` and ``manager_port_nr`` from the dictionary returned by :meth:`mpire.dashboard.start_dashboard`. Similarly to earlier, a custom port range can be provided: .. code-block:: bash $ mpire-dashboard --port-range 9000-9100 The benefit of starting a dashboard this way is that your dashboard keeps running in case of errors in your script. You will be able to see what the error was, when it occurred and where it occurred in your code. Connecting to an existing dashboard ----------------------------------- If you have started a dashboard elsewhere, you can connect to it using: .. code-block:: python from mpire.dashboard import connect_to_dashboard connect_to_dashboard(manager_port_nr=8081, manager_host='localhost') Make sure you use the ``manager_port_nr``, not the ``dashboard_port_nr`` in the examples above. You can connect to an existing dashboard on the same, but also on a remote machine (if the ports are open). If ``manager_host`` is omitted it will fall back to using ``'localhost'``. Using the dashboard ------------------- Once connected to a dashboard you don't need to change anything to your code. When you have enabled the use of a progress bar in your ``map`` call the progress bar will automatically register itself to the dashboard server and show up, like here: .. code-block:: python from mpire import WorkerPool from mpire.dashboard import connect_to_dashboard connect_to_dashboard(8099) def square(x): import time time.sleep(0.01) # To be able to show progress return x * x with WorkerPool(4) as pool: pool.map(square, range(10000), progress_bar=True) This will show something like: .. thumbnail:: mpire_dashboard.png :title: MPIRE dashboard You can click on a progress bar row to view details about the function that is called (which has already been done in the screenshot above). It will let you know when a ``KeyboardInterrupt`` signal was send to the running process: .. thumbnail:: mpire_dashboard_keyboard_interrupt.png :title: MPIRE dashboard - KeyboardInterrupt has been raised or show the traceback information in case of an exception: .. thumbnail:: mpire_dashboard_error.png :title: MPIRE dashboard - Error traceback In case you have enabled :ref:`worker insights` these insights will be shown real-time in the dashboard: .. thumbnail:: mpire_dashboard_insights.png :title: MPIRE dashboard - Worker insights Click on the ``Insights (click to expand/collapse)`` to either expand or collapse the insight details. The dashboard will refresh automatically every 0.5 seconds. Stack level ----------- By default, the dashboard will show information about the function that is called and where it is called from. However, in some cases where you have wrapped the function in another function, you might be less interested in the wrapper function and more interested in the function that is calling this wrapper. In such cases you can use :meth:`mpire.dashboard.set_stacklevel` to set the stack level. This is the number of levels in the stack to go back in order to find the frame that contains the function that is invoking MPIRE. For example: .. code-block:: python from mpire import WorkerPool from mpire.dashboard import set_stacklevel, start_dashboard class WorkerPoolWrapper: def __init__(self, n_jobs, progress_bar=True): self.n_jobs = n_jobs self.progress_bar = progress_bar def __call__(self, func, data): with WorkerPool(self.n_jobs) as pool: return pool.map(func, data, progress_bar=self.progress_bar) def square(x): return x * x if __name__ == '__main__': start_dashboard() executor = WorkerPoolWrapper(4, progress_bar=True) set_stacklevel(1) # default results = executor(square, range(10000)) set_stacklevel(2) results = executor(square, range(10000)) When you run this code you will see that the dashboard will show two progress bars. In both cases, the dashboard will show the ``square`` function as the function that is called. However, in the first case, it will show ``return pool.map(func, data, progress_bar=self.progress_bar)`` as the line where it is called from. In the second case, it will show the ``results = executor(square, range(10000))`` line. mpire-2.10.2/docs/usage/index.rst000066400000000000000000000001471461637447300166260ustar00rootroot00000000000000Usage ===== .. toctree:: :maxdepth: 2 workerpool/index map/index apply dashboard mpire-2.10.2/docs/usage/map/000077500000000000000000000000001461637447300155405ustar00rootroot00000000000000mpire-2.10.2/docs/usage/map/index.rst000066400000000000000000000004311461637447300173770ustar00rootroot00000000000000Map family ========== This section describes the different ways of interacting with a :obj:`mpire.WorkerPool` instance. .. toctree:: :maxdepth: 1 map progress_bar worker_init_exit task_chunking max_tasks_active worker_lifespan timeouts numpy mpire-2.10.2/docs/usage/map/map.rst000066400000000000000000000153451461637447300170570ustar00rootroot00000000000000map family of functions ======================= .. contents:: Contents :depth: 2 :local: :obj:`mpire.WorkerPool` implements four types of parallel ``map`` functions, being: :meth:`mpire.WorkerPool.map` Blocks until results are ready, results are ordered in the same way as the provided arguments. :meth:`mpire.WorkerPool.map_unordered` The same as :meth:`mpire.WorkerPool.map`, but results are ordered by task completion time. Usually faster than :meth:`mpire.WorkerPool.map`. :meth:`mpire.WorkerPool.imap` Lazy version of :meth:`mpire.WorkerPool.map`, returns a generator. The generator will give results back whenever new results are ready. Results are ordered in the same way as the provided arguments. :meth:`mpire.WorkerPool.imap_unordered` The same as :meth:`mpire.WorkerPool.imap`, but results are ordered by task completion time. Usually faster than :meth:`mpire.WorkerPool.imap`. When using a single worker the unordered versions are equivalent to their ordered counterparts. Iterable of arguments --------------------- Each ``map`` function should receive a function and an iterable of arguments, where the elements of the iterable can be single values or iterables that are unpacked as arguments. If an element is a dictionary, the ``(key, value)`` pairs will be unpacked with the ``**``-operator. .. code-block:: python def square(x): return x * x with WorkerPool(n_jobs=4) as pool: # 1. Square the numbers, results should be: [0, 1, 4, 9, 16, 25, ...] results = pool.map(square, range(100)) The first example should work as expected, the numbers are simply squared. MPIRE knows how many tasks there are because a ``range`` object implements the ``__len__`` method (see :ref:`Task chunking`). .. code-block:: python with WorkerPool(n_jobs=4) as pool: # 2. Square the numbers, results should be: [0, 1, 4, 9, 16, 25, ...] # Note: don't execute this, it will take a long time ... results = pool.map(square, range(int(1e30)), iterable_len=int(1e30), chunk_size=1) In the second example the ``1e30`` number is too large for Python: try calling ``len(range(int(1e30)))``, this will throw an ``OverflowError`` (don't get me started ...). Therefore, we must use the ``iterable_len`` parameter to let MPIRE know how large the tasks list is. We also have to specify a chunk size here as the chunk size should be lower than ``sys.maxsize``. .. code-block:: python def multiply(x, y): return x * y with WorkerPool(n_jobs=4) as pool: # 3. Multiply the numbers, results should be [0, 101, 204, 309, 416, ...] for result in pool.imap(multiply, zip(range(100), range(100, 200)), iterable_len=100): ... The third example shows an example of using multiple function arguments. Note that we use ``imap`` in this example, which allows us to process the results whenever they come available, not having to wait for all results to be ready. .. code-block:: python with WorkerPool(n_jobs=4) as pool: # 4. Multiply the numbers, results should be [0, 101, ...] for result in pool.imap(multiply, [{'x': 0, 'y': 100}, {'y': 101, 'x': 1}, ...]): ... The final example shows the use of an iterable of dictionaries. The (key, value) pairs are unpacked with the ``**``-operator, as you would expect. So it doesn't matter in what order the keys are stored. This should work for ``collection.OrderedDict`` objects as well. Circumvent argument unpacking ----------------------------- If you want to avoid unpacking and pass the tuples in example 3 or the dictionaries in example 4 as a whole, you can. We'll continue on example 4, but the workaround for example 3 is similar. Suppose we have the following function which expects a dictionary: .. code-block:: python def multiply_dict(d): return d['x'] * d['y'] Then you would have to convert the list of dictionaries to a list of single argument tuples, where each argument is a dictionary: .. code-block:: python with WorkerPool(n_jobs=4) as pool: # Multiply the numbers, results should be [0, 101, ...] for result in pool.imap(multiply_dict, [({'x': 0, 'y': 100},), ({'y': 101, 'x': 1},), ...]): ... There is a utility function available that does this transformation for you: .. code-block:: python from mpire.utils import make_single_arguments with WorkerPool(n_jobs=4) as pool: # Multiply the numbers, results should be [0, 101, ...] for result in pool.imap(multiply_dict, make_single_arguments([{'x': 0, 'y': 100}, {'y': 101, 'x': 1}, ...], generator=False)): ... :meth:`mpire.utils.make_single_arguments` expects an iterable of arguments and converts them to tuples accordingly. The second argument of this function specifies if you want the function to return a generator or a materialized list. If we would like to return a generator we would need to pass on the iterable length as well. .. _mixing-multiple-map-calls: Mixing ``map`` functions ------------------------ ``map`` functions cannot be used while another ``map`` function is still running. E.g., the following will raise an exception: .. code-block:: python with WorkerPool(n_jobs=4) as pool: imap_results = pool.imap(multiply, zip(range(100), range(100, 200)), iterable_len=100) next(imap_results) # We actually have to start the imap function # Will raise because the imap function is still running map_results = pool.map(square, range(100)) Make sure to first finish the ``imap`` function before starting a new ``map`` function. This holds for all ``map`` functions. Not exhausting a lazy ``imap`` function --------------------------------------- If you don't exhaust a lazy ``imap`` function, but do close the pool, the remaining tasks and results will be lost. E.g., the following will raise an exception: .. code-block:: python with WorkerPool(n_jobs=4) as pool: imap_results = pool.imap(multiply, zip(range(100), range(100, 200)), iterable_len=100) first_result = next(imap_results) # We actually have to start the imap function pool.terminate() # This will raise results = list(imap_results) Similarly, exiting the ``with`` block terminates the pool as well: .. code-block:: python with WorkerPool(n_jobs=4) as pool: imap_results = pool.imap(multiply, zip(range(100), range(100, 200)), iterable_len=100) first_result = next(imap_results) # We actually have to start the imap function # This will raise results = list(imap_results) mpire-2.10.2/docs/usage/map/max_tasks_active.rst000066400000000000000000000034061461637447300216220ustar00rootroot00000000000000.. _max_active_tasks: Maximum number of active tasks ============================== When you have tasks that take up a lot of memory you can do a few things: - Limit the number of jobs (i.e., the number of tasks currently being available to the workers, tasks that are in the queue ready to be processed). - Limit the number of active tasks The first option is the most obvious one to save memory when the processes themselves use up much memory. The second is convenient when the argument list takes up too much memory. For example, suppose you want to kick off an enormous amount of jobs (let's say a billion) of which the arguments take up 1 KB per task (e.g., large strings), then that task queue would take up ~1 TB of memory! In such cases, a good rule of thumb would be to have twice the amount of active chunks of tasks than there are jobs. This means that when all workers complete their task at the same time each would directly be able to continue with another task. When workers take on their new tasks the generator of tasks is iterated to the point that again there would be twice the amount of active chunks of tasks. In MPIRE, the maximum number of active tasks by default is set to ``n_jobs * chunk_size * 2``, so you don't have to tweak it for memory optimization. If, for whatever reason, you want to change this behavior, you can do so by setting the ``max_active_tasks`` parameter: .. code-block:: python with WorkerPool(n_jobs=4) as pool: results = pool.map(task, range(int(1e300)), iterable_len=int(1e300), chunk_size=int(1e5), max_tasks_active=4 * int(1e5)) .. note:: Setting the ``max_tasks_active`` parameter to a value lower than ``n_jobs * chunk_size`` can result in some workers not being able to do anything. mpire-2.10.2/docs/usage/map/numpy.rst000066400000000000000000000076611461637447300174540ustar00rootroot00000000000000Numpy arrays ============ .. contents:: Contents :depth: 2 :local: Chunking -------- Numpy arrays are treated a little bit differently when passed on to the ``map`` functions. Usually MPIRE uses ``itertools.islice`` for chunking, which depends on the ``__iter__`` special function of the container object. But applying that to numpy arrays: .. code-block:: python import numpy as np # Create random array arr = np.random.rand(10, 3) # Chunk the array using default chunking arr_iter = iter(arr) chunk_size = 3 while True: chunk = list(itertools.islice(arr_iter, chunk_size)) if chunk: yield chunk else: break would yield: .. code-block:: python [array([0.68438994, 0.9701514 , 0.40083965]), array([0.88428556, 0.2083905 , 0.61490443]), array([0.89249174, 0.39902235, 0.70762541])] [array([0.18850964, 0.1022777 , 0.41539432]), array([0.07327858, 0.18608165, 0.75862301]), array([0.69215651, 0.4211941 , 0.31029439])] [array([0.82571272, 0.72257819, 0.86079131]), array([0.91285817, 0.49398461, 0.27863929]), array([0.146981 , 0.84671211, 0.30122806])] [array([0.11783283, 0.12585031, 0.39864368])] In other words, each row of the array is now in its own array and each one of them is given to the target function individually. Instead, MPIRE will chunk them in to something more reasonable using numpy slicing instead: .. code-block:: python from mpire.utils import chunk_tasks for chunk in chunk_tasks(arr, chunk_size=chunk_size): print(repr(chunk)) Output: .. code-block:: python array([[0.68438994, 0.9701514 , 0.40083965], [0.88428556, 0.2083905 , 0.61490443], [0.89249174, 0.39902235, 0.70762541]]) array([[0.18850964, 0.1022777 , 0.41539432], [0.07327858, 0.18608165, 0.75862301], [0.69215651, 0.4211941 , 0.31029439]]) array([[0.82571272, 0.72257819, 0.86079131], [0.91285817, 0.49398461, 0.27863929], [0.146981 , 0.84671211, 0.30122806]]) array([[0.11783283, 0.12585031, 0.39864368]]) Each chunk is now a single numpy array containing as many rows as the chunk size, except for the last chunk as there aren't enough rows left. Return value ------------ When the user provided function returns numpy arrays and you're applying the :meth:`mpire.WorkerPool.map` function MPIRE will concatenate the resulting numpy arrays to a single array by default. For example: .. code-block:: python def add_five(x): return x + 5 with WorkerPool(n_jobs=4) as pool: results = pool.map(add_five, arr, chunk_size=chunk_size) will return: .. code-block:: python array([[5.68438994, 5.9701514 , 5.40083965], [5.88428556, 5.2083905 , 5.61490443], [5.89249174, 5.39902235, 5.70762541], [5.18850964, 5.1022777 , 5.41539432], [5.07327858, 5.18608165, 5.75862301], [5.69215651, 5.4211941 , 5.31029439], [5.82571272, 5.72257819, 5.86079131], [5.91285817, 5.49398461, 5.27863929], [5.146981 , 5.84671211, 5.30122806], [5.11783283, 5.12585031, 5.39864368]]) This behavior can be cancelled by using the ``concatenate_numpy_output`` flag: .. code-block:: python with WorkerPool(n_jobs=4) as pool: results = pool.map(add_five, arr, chunk_size=chunk_size, concatenate_numpy_output=False) This will return individual arrays: .. code-block:: python [array([[5.68438994, 5.9701514 , 5.40083965], [5.88428556, 5.2083905 , 5.61490443], [5.89249174, 5.39902235, 5.70762541]]), array([[5.18850964, 5.1022777 , 5.41539432], [5.07327858, 5.18608165, 5.75862301], [5.69215651, 5.4211941 , 5.31029439]]), array([[5.82571272, 5.72257819, 5.86079131], [5.91285817, 5.49398461, 5.27863929], [5.146981 , 5.84671211, 5.30122806]]), array([[5.11783283, 5.12585031, 5.39864368]])] mpire-2.10.2/docs/usage/map/progress_bar.rst000066400000000000000000000106111461637447300207610ustar00rootroot00000000000000Progress bar ============ .. contents:: Contents :depth: 2 :local: Progress bar support is added through the tqdm_ package (installed by default when installing MPIRE). The most easy way to include a progress bar is by enabling the ``progress_bar`` flag in any of the ``map`` functions: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.map(task, range(100), progress_bar=True) This will display a basic ``tqdm`` progress bar displaying the time elapsed and remaining, number of tasks completed (including a percentage value) and the speed (i.e., number of tasks completed per time unit). .. _progress_bar_style: Progress bar style ------------------ You can switch to a different progress bar style by changing the ``progress_bar_style`` parameter. For example, when you require a notebook widget use ``'notebook'`` as the style: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.map(task, range(100), progress_bar=True, progress_bar_style='notebook') The available styles are: - ``None``: use the default style (= ``'std'`` , see below) - ``'std'``: use the standard ``tqdm`` progress bar - ``'rich'``: use the rich progress bar (requires the ``rich`` package to be installed, see :ref:`richdep`) - ``'notebook'``: use the Jupyter notebook widget - ``'dashboard'``: use only the progress bar on the dashboard When in a terminal and using the ``'notebook'`` style, the progress bar will behave weirdly. This is not recommended. .. note:: If you run into problems with getting the progress bar to work in a Jupyter notebook (with ``'notebook'`` style), have a look at :ref:`troubleshooting_progress_bar`. Changing the default style ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can change the default style by setting the :obj:`mpire.tqdm_utils.PROGRESS_BAR_DEFAULT_STYLE` variable: .. code-block:: python import mpire.tqdm_utils mpire.tqdm_utils.PROGRESS_BAR_DEFAULT_STYLE = 'notebook' .. _tqdm: https://pypi.python.org/pypi/tqdm Progress bar options -------------------- The ``tqdm`` progress bar can be configured using the ``progress_bar_options`` parameter. This parameter accepts a dictionary with keyword arguments that will be passed to the ``tqdm`` constructor. Some options in ``tqdm`` will be overwritten by MPIRE. These include the ``iterable``, ``total`` and ``leave`` parameters. The ``iterable`` is set to the iterable passed on to the ``map`` function. The ``total`` parameter is set to the number of tasks to be completed. The ``leave`` parameter is always set to ``True``. Some other parameters have a default value assigned to them, but can be overwritten by the user. Here's an example where we change the description, the units, and the colour of the progress bar: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.map(some_func, some_data, progress_bar=True, progress_bar_options={'desc': 'Processing', 'unit': 'items', 'colour': 'green'}) For a complete list of available options, check out the `tqdm docs`_. .. _`tqdm docs`: https://tqdm.github.io/docs/tqdm/#__init__ Progress bar position ~~~~~~~~~~~~~~~~~~~~~ You can easily print a progress bar on a different position on the terminal using the ``position`` parameter of ``tqdm``, which facilitates the use of multiple progress bars. Here's an example of using multiple progress bars using nested WorkerPools: .. code-block:: python def dispatcher(worker_id, X): with WorkerPool(n_jobs=4) as nested_pool: return nested_pool.map(task, X, progress_bar=True, progress_bar_options={'position': worker_id + 1}) def main(): with WorkerPool(n_jobs=4, daemon=False, pass_worker_id=True) as pool: pool.map(dispatcher, ((range(x, x + 100),) for x in range(100)), iterable_len=100, n_splits=4, progress_bar=True) main() We use ``worker_id + 1`` here because the worker IDs start at zero and we reserve position 0 for the progress bar of the main WorkerPool (which is the default). It goes without saying that you shouldn't specify the same progress bar position multiple times. .. note:: When using the ``rich`` progress bar style, the ``position`` parameter cannot be used. An exception will be raised when trying to do so. .. note:: Most progress bar options are completely ignored when in a Jupyter/IPython notebook session or in the MPIRE dashboard. mpire-2.10.2/docs/usage/map/task_chunking.rst000066400000000000000000000052621461637447300211270ustar00rootroot00000000000000.. _Task chunking: Task chunking ============= .. contents:: Contents :depth: 2 :local: By default, MPIRE chunks the given tasks in to ``64 * n_jobs`` chunks. Each worker is given one chunk of tasks at a time before returning its results. This usually makes processing faster when you have rather small tasks (computation wise) and results are pickled/unpickled when they are send to a worker or main process. Chunking the tasks and results ensures that each process has to pickle/unpickle less often. However, to determine the number of tasks in the argument list the iterable should implement the ``__len__`` method, which is available in default containers like ``list`` or ``tuple``, but isn't available in most generator objects (the ``range`` object is one of the exceptions). To allow working with generators each ``map`` function has the option to pass the iterable length: .. code-block:: python with WorkerPool(n_jobs=4) as pool: # 1. This will issue a warning and sets the chunk size to 1 results = pool.map(square, ((x,) for x in range(1000))) # 2. This will issue a warning as well and sets the chunk size to 1 results = pool.map(square, ((x,) for x in range(1000)), n_splits=4) # 3. Square the numbers using a generator using a specific number of splits results = pool.map(square, ((x,) for x in range(1000)), iterable_len=1000, n_splits=4) # 4. Square the numbers using a generator using automatic chunking results = pool.map(square, ((x,) for x in range(1000)), iterable_len=1000) # 5. Square the numbers using a generator using a fixed chunk size results = pool.map(square, ((x,) for x in range(1000)), chunk_size=4) In the first two examples the function call will issue a warning because MPIRE doesn't know how large the chunks should be as the total number of tasks is unknown, therefore it will fall back to a chunk size of 1. The third example should work as expected where 4 chunks are used. The fourth example uses 256 chunks (the default 64 times the number of workers). The last example uses a fixed chunk size of four, so MPIRE doesn't need to know the iterable length. You can also call the chunk function manually: .. code-block:: python from mpire.utils import chunk_tasks # Convert to list because chunk_tasks returns a generator print(list(chunk_tasks(range(10), n_splits=3))) print(list(chunk_tasks(range(10), chunk_size=2.5))) print(list(chunk_tasks((x for x in range(10)), iterable_len=10, n_splits=6))) will output: .. code-block:: python [(0, 1, 2, 3), (4, 5, 6), (7, 8, 9)] [(0, 1, 2), (3, 4), (5, 6, 7), (8, 9)] [(0, 1), (2, 3), (4,), (5, 6), (7, 8), (9,)] mpire-2.10.2/docs/usage/map/timeouts.rst000066400000000000000000000035111461637447300201430ustar00rootroot00000000000000.. _timeouts: Timeouts ======== Timeouts can be set separately for the target, ``worker_init`` and ``worker_exit`` functions. When a timeout has been set and reached, it will throw a ``TimeoutError``: .. code-block:: python # Will raise TimeoutError, provided that the target function takes longer # than half a second to complete with WorkerPool(n_jobs=5) as pool: pool.map(time_consuming_function, range(10), task_timeout=0.5) # Will raise TimeoutError, provided that the worker_init function takes longer # than 3 seconds to complete or the worker_exit function takes longer than # 150.5 seconds to complete with WorkerPool(n_jobs=5) as pool: pool.map(time_consuming_function, range(10), worker_init=init, worker_exit=exit_, worker_init_timeout=3.0, worker_exit_timeout=150.5) Use ``None`` (=default) to disable timeouts. ``imap`` and ``imap_unordered`` ------------------------------- When you're using one of the lazy map functions (e.g., ``imap`` or ``imap_unordered``) then an exception will only be raised when the function is actually running. E.g. when executing: .. code-block:: python with WorkerPool(n_jobs=5) as pool: results = pool.imap(time_consuming_function, range(10), task_timeout=0.5) this will never raise. This is because ``imap`` and ``imap_unordered`` return a generator object, which stops executing until it gets the trigger to go beyond the ``yield`` statement. When iterating through the results, it will raise as expected: .. code-block:: python with WorkerPool(n_jobs=5) as pool: results = pool.imap(time_consuming_function, range(10), task_timeout=0.5) for result in results: ... Threading --------- When using ``threading`` as start method MPIRE won't be able to interrupt certain functions, like ``time.sleep``.mpire-2.10.2/docs/usage/map/worker_init_exit.rst000066400000000000000000000041141461637447300216570ustar00rootroot00000000000000.. _worker_init_exit: Worker init and exit ==================== When you want to initialize a worker you can make use of the ``worker_init`` parameter of any ``map`` function. This will call the initialization function only once per worker. Similarly, if you need to clean up the worker at the end of its lifecycle you can use the ``worker_exit`` parameter. Additionally, the exit function can return anything you like, which can be collected using :meth:`mpire.WorkerPool.get_exit_results` after the workers are done. Both init and exit functions receive the worker ID, shared objects, and worker state in the same way as the task function does, given they're enabled. For example: .. code-block:: python def init_func(worker_state): # Initialize a counter for each worker worker_state['count_even'] = 0 def square_and_count_even(worker_state, x): # Count number of even numbers and return the square if x % 2 == 0: worker_state['count_even'] += 1 return x * x def exit_func(worker_state): # Return the counter return worker_state['count_even'] with WorkerPool(n_jobs=4, use_worker_state=True) as pool: pool.map(square_and_count_even, range(100), worker_init=init_func, worker_exit=exit_func) print(pool.get_exit_results()) # Output, e.g.: [13, 13, 12, 12] print(sum(pool.get_exit_results())) # Output: 50 .. important:: When the ``worker_lifespan`` option is used to restart workers during execution, the exit function will be called for the worker that's shutting down and the init function will be called again for the new worker. Therefore, the number of elements in the list that's returned from :meth:`mpire.WorkerPool.get_exit_results` does not always equal ``n_jobs``. .. important:: When ``keep_alive`` is enabled the workers won't be terminated after a ``map`` call. This means the exit function won't be called until it's time for cleaning up the entire pool. You will have to explicitly call :meth:`mpire.WorkerPool.stop_and_join` to receive the exit results. mpire-2.10.2/docs/usage/map/worker_lifespan.rst000066400000000000000000000015301461637447300214630ustar00rootroot00000000000000Worker lifespan =============== Occasionally, workers that process multiple, memory intensive tasks do not release their used up memory properly, which results in memory usage building up. This is not a bug in MPIRE, but a consequence of Python's poor garbage collection. To avoid this type of problem you can set the worker lifespan: the number of tasks after which a worker should restart. .. code-block:: python with WorkerPool(n_jobs=4) as pool: results = pool.map(task, range(100), worker_lifespan=1, chunk_size=1) In this example each worker is restarted after finishing a single task. .. note:: When the worker lifespan has been reached, a worker will finish the current chunk of tasks before restarting. I.e., based on the ``chunk_size`` a worker could end up completing more tasks than is allowed by the worker lifespan. mpire-2.10.2/docs/usage/mpire_dashboard.png000066400000000000000000002024341461637447300206210ustar00rootroot00000000000000PNG  IHDRR! ~zTXtRaw profile type exifxY$ Dy 8fz*uU,ӗ2-#\@8pV8 PE{E>uԑ_{)|x/aXL?{2䇉z__mȟ{׋^ۊm0rڿߊNT"\ˀи蘞g:==V|>ЊW@Wg/ۓ|kOrY8owڝՆK{S[|8qyy5Ox;@. qw2TNn:}5s^mhTn2.Z-Yw<˭Yx'zd9Dzȧ/_aW Gίt?H|򸹳A5Ŕ-](_t'E- Ė)>D|HXk![4k3F7K~5Y!$(Vb#~vbȤH&*]X+6iis3-ZUjסK]z{ȣ@2f,jl6:<ˬSf:ՖX.m{l;J9g-^w\D-?}@y{Z--tG=m{;Dvqyr {L6QV)s5Y#ټ{akcؗuV,<,Oa{S F+jH\[x .jzם LRWwmoe0k%4mlDT.ɡ6ԒliZK$i'U '׎5gny1;•II$#̊m-5C ! D1UDĵt{aAY#P36'چx|dmLD!(Zs%SlٜVks`aY6ifU.NyhM5˶y6KI6-ɔUG3r9 dP֒u#:;$.JG9Ԩ(( ;u Ͼd}OwA?Dzٞ9z iUGY'՞!ZN ?؇ e~8Z ~ ܁~ML:=d.Èwp`>~qk=Iզ+OF#_|*w]VKjbEV= *#js'7Φu/]S ^L!Z Cr׭PWb[nV'ֆ<}H rԠX&i,N!\']BXbER?4#)AgPyTt5gDƦSh*;)?'qd2*>0f={* P|H%V(yCLc*}d ?Ս@P#X(Qu%RAVUվ2gp9p!e?=@PAltTaJ=M4u:g's7V7 i7D'{Шz-i.݉+ B|pRgt(Jl> Z`wR"s~nTy`9~*HTC܍gs)-wZ(x(bP~MXRW%w+lMJ$ۥA)J(Z`A  tO9j#$f#( ?a]kLAv= ɵj)dZFbD!sɏNi4 :}#bm Y "H;G kam@2\OSvU;9 FU:VPA4͋su~W1'h~e?5/apѨ-L}6* `@iCCPICC profile(}=H@_S" vqP,_8jP! :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽjiV(鶙tfE F2YIJwQܟ#f-Df6:Ԧmp'tAG+qλ,̰J|+M F%c@| ]VqcǩgJoKU`JC=uCSɐMٕ4\x?o}@ת[}@JpZkVr?עbKGDC pHYs  tIMEDfG IDATxw|gf7Jw!w(l`A׆zr*^xH :@HB;&dwvΜݝg9Fly&|0`#G A >|0`#G A >|0`#G A >|0`#G A >|0'ZNUUOa6ڶ-WV߯WlTvm2$w0 FڒtՇ4 UV_/C2dJjWlN?G0}^L >|0`#̊O?;ox\\^?4w}OE Oyhjּ%۶mݖ2*t{bbbtN_~}=3sfiS o*44ݲ,efe)-5UI:|萶ldYEBŊW5NLL+/Y Nk}ʗ/Jaa$""J1 CNCNglٲjРM'%&&i P`@J.ڵjstQ};n<\C| -Zi[h?Yp {aA{TXQO?U\C͚)S.{"E?tB7MU"$D5QXX%s%JQ#v8QoxSAAX7m>!Ck*WVEro> ֮'ܗoX˥ڰyD.S%dS(5'&&tҒ2eʨjj:eͣIӦszv++KN?^M7)q9}Zi{W|VIڵҥKؾl[<~ll.]=$*]ZڷӒ_eimt4g.j5:$IWJ޵֮O,;]M@ޥ濣)-[yoڼ/YիVjinا7o֤I k޼%g 1l"Ҽߖo&zԾ ;fk[~ZuEԼY3Ml۾鋗(:uHΤi֭j֬_~S=i[z59W/V\oSNCTJ֬ۨmZM˦Zx$4MujFm[6SR!JJ:EVhuLTNզe3(VT'5{~6o.I^+vkW^M1W'S|r?GGGy,5k^[6ünZ\l333u<.0  *iffx{gڶcB˔V Iݷ-ݻhGwG7th睦-usΊ^Q_}>ݩU( @}xMNKOm[6ӥ>-I*VU&3Am[-ɶeٶ,w@*RzUIM1ݰY[4UJ/4ܳ>=VT$0PiizAZaح{kϾ>߮8:*+^\u;.8M.+%"/}|!뮻N7lU.NN'td˖mٲlKfgW[󆌂_UX]b۶-˲eYnݖn  %K䩥T$t|JM$HɑZ7u6-m˽ k>>z=+IZ4oq pnk[),L;(߰5fT,$DԲEKE4noll֭9 CM5v&U;v켢ۘз {Flj *Uŗ^[W/EWv_ڵdYt29Y;weu`uszC`4A7s{Έ!pvh0LaȐ me95yZmɲZ%۔adHrki7_yIRҟwZBұJKOיtIRJ9u;NLϤys X^2 >rLǎ뛟~iY:o7wSwC/lDtT'Ijܤ~'ǭZGȑ#z!??+JԸqcjZ;NRR,YzE[H}?4C_\rN?? y߽7s GyߢP4 Co]]tݻhB?!˲͚酗^Լ4}Tow)??9gn95x6rV95|}^syzn,ްײ,ٖ%˶_K\t::zՉ^=Ҧ55W*auI mM{Õ#G ˲:r,;΀[Dboр[h+-=]SbR"W%-AD6: Vzum*5QxozwOҘ/G+++ٗ\3g*33'c6=zGcw{nvi%jР23jٞO~~Z[XS7_7W 8kffз>}g]S i9_Oϖe4]'IfFnѩ<ïҕﯪ+aݻ_Vy2DEۨ=Ouuku>g\.e|uw(۶uQ5h5qL%La֢+,ujZڹ{O[O8}{z꒤-Z͛K>QuNLLWcԁ/..NԶmiΤ[0m۵Sn7LhΤjhM2EY.^|O?=ii)ڻS0TRE3P*UR|\N-[<`N3~3Vvl[]Td)%̙3~:#͘6MuU뮓Һk5ie\ ƍըq#UN]*V۷iĉ(}9 }՗Z9N9ؙ S6m^F!AOܿak 3'5 }O_;&qo mmYn,q.;2:ͭĤdM5OVEkIӔ|2EZ5WJ8'Mʨw&~[~a/wO?5wԮU[ W3gL޽{UByzmr\:eV^[oMEu&FukתdRo k̙б{ zM>tȻ~nX$P'NP~tݵp|޵Szhc rf͜)U*UtD -?/{wh_O>˗iijպdz*)1QvT6m4stNGvi:Ր{n[K|sN`375*Ν޳v88./crm2ML!w}rv4Li ܽGO>%v5so9BDZ,KsDj΂>rfL~Zv2MCEՠaCmܸ18[ޏ^,w:zV eddԩSJHH(+rk΂*-\Νow!0UVM >\?|VZ*djԪ4͛;Wei<孵vMn65mL˖zjnѪnxHM2ųh7գG}5&/KhbOSh1uv~mLUe T~۶mӼժu5 o/Vi5s:uϠA~$m߾]#3iJÎ;?0MS`,{yL[P6]Οs`˲e=t{d6mfvk2Lw\|&NII;Un=IR-NCM6ukr7m Ԯ%=I{233 k=ӷ^?nZmU+AڶuZheK*(8H 뛱_ڵei jܤIq \VM9mܰ>OsRLLZj%K)99It|9}ꔊWoXڵk\YYjҴJ.goÙ{g+H}zBX4ܵr=y7sUn3yjv!Am9۶dRٲM-3a_ ,IڶuΤ_˖)ReuId?NH vn}٧ҵnK'NhO?y^j)s[tNՐR%=~Zv&M)Zvl}\By%&&o<0˔dIeW@ 4,Im -ٶQ9 y=+ly'8{\C/1>oXNwu0<[E\}_QUFCC֛#GlFb(&&FjРnK yXÆ=+WVb6oRZz4nFZ=.3gR |qzF$?ɓ'[?~|a$,ʕ4\8jެ&/\eYlKe6LY _=[݇q~Fm5t^][ؖ9a熿MPۅTu<m[<5s?;baƤI֭[[h!vGDHfdhͼ\YYf8Yo}? ׭o +H<#\n[V-ZRufSV|׿Nv,t{m[*]8ཅ-e]f_II -#I?2eʨhQO-c0t<(0uJIIц %IevrveyZ`ܷ՜ayXnKn}myrs-Ke97;Mc{ߝgu\n.|snK+8}mEG)";mа'mX~~ 6*++W>CjyޣwXvzv۾{NuC7Ԟ=:Y5+WJLLԞs-,Nյۍ*Wƍ[rSNK*}{*,nGQަ/նm[կ?*UJQk֨=ēCz*5nD+V$mN5jTr n\.Lk*hSM+{5mg=2 뼾xmͩS]7)'Xl[n0 >oڸQE磣V`jҴʕ+6p@1;%۶mӸcƛԲu+eeI'M7}{u&팢֬)謙34TTi>tH~=reO0^)N]qsOJ9kάYG־tǀ3zs pK"5Сԩnݢoƍٌ \YGڶNLӔ'fI\l9ntn|~1כgι<+op{B_O`pNe_sLlެf=oTn=IS}^d\?p{j>QkV+jͅPz5k9?$iҥNwlҜٳ5gB:o}W?|&Y|ϿK5k)6vƍ+%oYn&/pϳy`[9i2LC o`3m70 #o?yoK5 w~}s7D>7vri.ڢ> 4h;|Z6AOavڥ+W]a7tFG_6ijw>rE+5"\SeO[jUEDDU6QWw1޻z7mؾ]'O&Llrj5jhܹ9c$ɕRN^ra:d6r߼>rnnsڜsj_ȭ_7=o {n9M^<ܞV`Vf\mw'7櫝O[(-=MAE 狛2WEGX$á{y_lAN=А?yWtv|BP\ڵ?9Q#GQjڴt%JOi銏ӎ;5nJJLNrɖVÑsvykX_VZy175'(>7ͷ>Yvn!3/˖-MA{aǞc:) pee,Wٺ&Nrk jۮwX||ϙxgh kCZ8ږЩ:u]SC,K֯׆/y'OrK :#Wڵe؆կ髯AttTky­֖-[ ۯBJd@p\{wVVV54e۶2wn%$h޶ >]|0`#G A >|0`#G A >|0`#%(ů IDATG A >|0`#G A >|0`#G A >|0`#G A >y`ҡJ ɉ W9فh|0`#G A >|0`#G A =fqU(*=V2-e#F)p"_6zm}w_k|cMUVTtU_ P߳ΆzfoZC,QӚ%xz*18c|?]HkҜFa!ma_~wzU)([/Ԋ uEZ6_Kf|ϟQ5ƒiz_}O7A-ʅS45 D5@տMܲ&.>^ŏW 3>35sg<PfL{Z-.>K~p#/^CW: zSͣdjuβ^C[oVUK>Zb~KTo.RRjZ1q%jŦ#鍲uzRM[U]5) VjodUKwm)2BՠU U5~WֳڿzǛ ,YUMZֽWWӈ)_Waչ^qp/Y rjU5NѳuetjjwKzYǦ]khIpG<^MRvi5J)RSmg-A/iډjЮ*re#3SqkAS/zUEߠ 7/{˧ږUz{!]uJ>2ؤzVos(oΆzfGBO?E+--˺f%/*wVCZ0CM$#Hie>}Tg}W* P͞O:A"NŜqzH`Qg*yzxre矹CkV@I0G;N+(Xq'{%)oߨM*`ZY=>}?P6Vڧ4G5z_v W墙Yo>i%9+\'^|TK*}})LOUYӟKKߍO~KgHj+}]'u>V~F'rVPPa >{b Y}%I.I6{R}W7]VMTMoj+͵;̓2*|%*]Mآ [+dG8bZ<Z_m9S >лQ)?FK뽣^oijI.?ootq[A}WQƬ=)(;Ty\'Wj4qjb*vm7]0e=?%QTK>Xx')zG]}xK=úqBJ]o@vf:|ecɯe!Jd*z>jZ*]Ү"R/C1?[+aPuCM6#^ojU?ԾzNnF|ȸ /t}3(f԰U:cɱʐVX'=9a- 57Ո54v3`w 9 dh.=:8CoSuKzmPا_bS?ɝU>\wf}hF>_>3`G;Юxd>~+OwuȬS?wO҈Og-C@yq\g}gZGխX5pB! 9˪a 6lII+:UM=G+S?os4uE|0\˚YY^}Iw7׎)P pz Vhk~9r/~j|Q*8T;*iGzO4DyKg+uSP .5P0˪]r:sFnV}kzPKͲiڴZNDM'S)rW=T'c&]M5w\bW(PvA|>unx~~=C5*T_K.R*J?VM?J mC,F]+nҘwS>^ISw߅貎=*Mԫ-tUÔ[?謰Jiwz҉etơOԓOJ F"в*SNmUFG;o[\2WTS=^X<4EÞH}֍-v=ucMP̦HX]s!C2^G1e:u&PUCwBKRuMl<ʃ^,ZIy-:uz磲vuh@i^+Oer8+F{~63+}da / 7/p}QVd֘_ifC{vSU)ԩ*-zMuP_襏=CGw-pT"j3UW'60QkN^)kus-SsieXt\}^};y'E | zeNh/+ik Ճ]&;KuPM_8c^Kv+_ct:v"uVZ-ec>h*박wovT ʖ/#gH)N?ݷu9!K,V8<J`u}m:ڞ-0TN;B؍7hu]4v+e4\paX)JSjz85狪,I2Kǎ{WguK* J?%fʇӞOc{7V2T:g6)%d?$9LjO̕z׵  Q:mKFpi 4u]piUP2%>Sd%;EI'<62Md tYǞ-+%E)$ڒaTBJt1Gue_ -bkTv#uQ=A4`=ynaҍmr%߅/ZIy7ɔ[ǏL8K"\izV& ~8|J$Yx<oΞәn2%##CCNDž G*aJϾF{Vǎ&V_ ԓkB$|, )axzjX[]r)XmnALQ~Ƿ`MbZM'v_>$k6mbbZ<)=4~}_*m,RDg뭇?QRjڶV?o9yHݬ_zI݆ܩ +h;X4ft[j>ZNa$iu7o;+Kiu݆ԨPsYJ֨2:aa 1+RdVVfVv@k)9)E=7eI+Fp/gѲڷ`*N5"{ u_Z1ly:E \6Zղ!-zƖmI2 CQBW2=bp~6@c֥)-ۖm۲%efVNX2PbbSljFq:aI%%ɶdYv7e*a8y}\ =`m֟IVY[#ksoLrR>NuCt΁aw&S;,og&Q^U+9L9ª?b:QרN5!vt a*[>T;Y=GaHkGy:+wȸ>mt[yV:dȔQ}o#ᖟ!g[֧w޹Z!C)#OlQԦHEGzГwT-0fdtS)~Ǘjꪁjt}ZrlyQVi ogE#b2ts?_H QUꞦn|A}_y~G~o'Ȗ)>GOQ.TmO%GpyEjj׬%9*r躾CtGR>6wHJ^)~w3ҕn*n M.3^͝U^?~U לR7:ӯ,I> Y:2#}y5gYިKյ{w=~K;ߠ]k2,m\{#e.< nV%QVM:R7(lfvVx&*Zrlڲu垲[~Np+hy5ڕ0e XfB疍_/)ZUڨӝOip\%7C*9$z~9r¯>kV-j^bRBϛY9e.ЕFv6lihB޿LvEmQ5zGv1em闧#ԫw}8)SJת,^kЌ>mw*mQ\[ij_=z*^fOFysp7qoV+#na~FKƼK>?P6ԿoOҩ7=y.L{6+zfLԒ >_%gꊍ/jW{bM>&w6Ueϗok h~wܨAT*5}CjʇjSSz\wTI_;-dlW5Y.DžάR/}L"ԻWNVeS[*c+ugZP]us :NQ'e+MѳhGrq5z_?~jxxEm;+bM*x)疍EvE}GQ5~gS H 6 X.bAT"b>+"X* i@$ \W$fg̜3^;o#ѾY~j6D2?.L.ݹmQliUއFXwz:U? k]'8}I OJ%쀚NJ` l}H;v,z||3`OsYI9<YW%I$I;{Z9u2-#|,w3m>N$Ii% $xϱ\Z~*e:rJ$I{$;̛IIgO$Iک%ԩ߰AҎߧnMv٦I½9H$I7w*sdִ? %)?]/Ma$Iک9$I$I$I$ʼn@$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)N$+QGI$I$I$IaՊz,I$I$I$Iqb'$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I'G _>$I_^^ȳϿi!$I%ь݇J$I;~(O{=h$IeM+ ~%IPJJ ^|;B$Iv2WVs'H$I;hN$I޿$IҎ+%%ŝ I$I;P FȣWN-<xO?/{D̈́iѧ/3Ǝ4N[|2_ߍDNJ[^T:&>1Gu>#e0j'n4Ty=}mavmta0l?H+K#xXGc`Rjxxϒ@`ɐ$Iv?~z++HoI3!9nWR|*49ϽcF18w@hy1z?LSYʈݶP^Y+xk6,f‹#gwy8}Q֕V IDAT?/_yù{UT/k >+2/~n6-mW}z7;z8oڐo|G}ȸ_S?iw%D[1n;elӾ [gqַ]uԌn{w??{WwMϭ^W-.'[[jN^>ba?ɦm_mSHލN-m6$mvz`];έG|nU%xw&tp {<^,Kg%T7q8q{ˬ;zAe_w;'Fדdbw^#Bpig}yOL^Ō7UWdᷓ´j{CzrDFP;zI 4)+j035}=n $3a9ZNK p][xYh5iDu(<=ֺ{%I *9%U.;d4W=o>)-|@'*x 'u<^4rLȂ.ȞgNm=s6_"y*k$ro0m!fYKh/Sg7vsq5e[Bf-j$?%R| TYRR6c_xf`SӚЬf(fYK^q d[I&Emۮ:DcфgxeGr9^-f4im8^{-P=^z8Vb[15wE|Mx~ Ѳ׈X󉵯# {7!Ԣ+=Ctc5hJyٷyU!fߒۤ) bXן>n[r>ٶ%eGNJG^aV^sD̵=59fٵ=q5>i,(Iu+ZVtϹڱ\uj<uHwq'{q敏ќڲx |gnR}= YSgS'#&P|1fZL|~9Mv~& $|s-tT@#{9w/&o"Vy??Ko񯗔 եa5̙b%{Vn@k-ΝOn"糠6I1͏)΃ ?w/*pBM"0t}. /+0!u9^B}oE$IU!3DLZ ZȜM]q ?o)4KV?5U#}mlD /b7^b.\G<;lX*k1^<zT:x_Rfh3Vyٞc=w]'Rxw*YIf灼4b8<ߗ3jfǾ&ǪS7GN.ᦗbeyP=N eB/oZΆxKIsv+""$ds7s9#Lk݋[:(us{癴; :{niOMeizڹ?}8[nqq@+nCgEd|Kp RIK'7DsON!%4IK P擛BjJJiNTW{΃?Bl84=zdpSpsp\hsTSWpV5$IQg: o1?}=?G R+Kn=/7RRH ŞO$wMH@jT#wӯ擛) NXrW,c ʪ1,1R֢a~*r?Unv\S+m[rC[lKPr%R<3>ob;y4NJ_۷}NڞekbZ&[spgh*oMԌ ?DufAqpEU}{v >=;qD"Q"gKPxIڋo?/8 خϲDO&5$ SH@^i%9(A2Iyǘviept)BFsVk ap9lFпn<FN ~B$Iyo_@^R % HIM\6Du>Qr󊃼?K&%r7|9Ԡ'4Gþ;aU4l[ؖuIm>bԴ-csyz+уIv<~g;xj%mRǀ7| ?5u[iJiߏ/Ǎq9fA69_C~f_psp0mlJ?)ϸO$: )bC1n\fL΃#Ѳm+նh~#8??$7E(/(~DVpQ.UUWwo eЦ= 97'{ mRik8_|) b\ ?w3j'>cb'{$s` yj,͙ 8z):u8#(כIhGпPnzA=6CkR39DTOLh%Cn%˹{B%#HJ" MrЉ9.;?ל۷ґ0CW"(գڢ__cZ sRA]K.^z aq>ʝ_FY0l*P)(F =H s){o剰`*[$Iw6{t,|bך!W?eYz|ծOXY$f:7rcCh:D_?bJojwbm aVWZYԬA%a"aΧ}gJ iZ@.~Gls痊h.˗9z0Jh#a #Q"qk7F'@ڥ1{ga^$Jd…aDË(Jҍ0?!*zI3̉ Ia7K [w$jqU/6OW,PY1ANB!{YBE_^}+/"7* {a D?BE)-9w_=Uݹ޴16hdf֠F"ŭL Q~[.?sw~;45].`j$g%]OhޜYk¾m{2,S^+ܽW,*1-fر,=|Arj]_r &~7k cL!Jq^qgK’P=Cd;fՃ~͡b(RJ;gB$Wՠ:Xi$IR٭R9?ɄLN(VHj˳r:FIx+ eECP-?aLЁhJzRMNRzҼeCjY*k ڵ$p̶e-H6ON. %Ves.CxNuȝG@=S#ʘUYTL]j;esy\d 3s2Rc7F?5f5B~s~!]FRB::ói9\0h>0gSt7|C]θhHs(]NǏc&&Ӯyke'K8V pa*Oee<7yU irUtj#>[A;AbArHyq6a|f"?0ֹ[E'd}3!V}RAev?0}A0SƦC3 =!]Z9{)smʹNk{i<)3kdVgphޏO.y8yc\ף'}T|7r-1T_zvբYL1|E}CԩW Y]\2{Ԙ߿y^߻n QED 4fc:U dHVIqU8"$ g7pƛQ ] Ͽ ,.}];(P}_ ྷ!H$IP\u)O[˘GdHY?Osӊb'9c-7dW}`޻yᅾOB5W;`%6ﬢZ"O; [Fm!VYK1o{]bxa[w';>8^,} yF6ǝמZ\;U\z)O+%g9(#2N[){W_7=؍o~5B>o5.Qw#vO*-3h2"$i7qGG8궱.œwЯut-L#߰65/Vɥ=-ɬ y 6 =uWy%{\wL( Jc^c'f}PF:qY,lq4cj?``IG+3utw$I$D|e/r$A$I,// M_2$I=%I$I$I$)N`I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IRHW0z $I$I$IªY;,E$I$I$I Z$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0jV"pOH$I$I$i`[I=Ət_c_m̅%< j~ SnDJh/G$Abs&$I$I$IT@ ~fx1vïyDhrwؕNB6jd=~?fz%I$I$I1|,[ |(\%KRN?+$5O{G,&PzЭdEY3{=8o]f A>\qm>Qz>8kT-WLǽ^bԀWu^R]}8dƃ\_sQY,~#u}'#a9s崽/7 "oAcX_dtN8?]cf'Zўkmpgyb’$I$I$I~hִO?cS xCe 2 a9v|Sq(Q mO:>>M!+%$v8Ϣ('AmRhuܑT~j_=+ǾG7էfWd :5WIIF)ZN$I$I$I} =nyo:+ !qު2 є4k{\)`$;;{͏ӻ |wӻ,\&dV `uȨ^hh+W;g Tr`71ݹly/#TTC<}Ҩxz~ WO?ZCR I$I$I; Ҩ!wj !^sd&$ J:O5,[Ha6rc «rLl؅NKf3q^#hHZt;}OF2qG]ŭk$P |a?4Q Y)TͪuDTщN$!1!\~p "Xh %I$I$IW$^~3nbeLS  C:|uzDY ~Qˣ$MdX<ݎΩ);Ή? IDATtd> Vo`W(' aL%f.F 䡏W!`Xջ/ }}{sXx>|>֫K`712]?TG}(\᯲=$I$I$InAC ̨%I$I$I$ wshI$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)NK$I$I$IR0$I$I$I8a,I$I$I$IqX$I$I$I$I$I$I$ `I$I$I$I$I$I$I' %I$I$I$)N$ KrMjD4;ΐMKPB!dbP PɿPѢH R}/|~A(7Zj~h (]z}7.4ZߋJ׻GK{ui6ی^~e}D"ElPK$I$I 50^IM)aNV3\Ugghwμn'?+|}nCwc8GeIs\:18ckcD !HHFKP MA.$JѲ+RN/5X/3Yxi#fAظޢ s`{?-Wh8 $I$I$__jdJ`k/H#RBq(W/#axGe0e;hj}eImQs!xH9? ^?xm!$&MHPQ\lwc/Pq̓(H RgE켂`AK܍FJ6!DKu sGq޿ſ* C pԺ ~#RAkTq>3ݴrz,ۥR6,/}4 pz B-?w&D[˗寐?MI'~zFZ~;K1P{d?t,r^F ?,~)ֽyM,?)oŒ]C$l@{ v.$_nPC(v U $I$IcoSeY,L.lF׻riݹ~)KogfhwB񬒚qױϷpm?v;6d_Kb,{N߻ âfv%TxQ^DPnaQR/&5҂? 3*j@vae4̼tya"+нԞ $ڧXI* ^e *=;LY%>0붯m =t'^ʠ_v Ѻ>&u/mu]1nzqz5~Τ֧?-#?mm@~˙}x!U?J *rVnf~ Ʃ7^k<}zӏKiсGΥu3KHE]Їe'2H_r ʚ3tBD|FFRn~&g6WebId:%_s$I ZH棏RsV^HayT[%R'hb"$%PޭA޳@3*ӓ6[JycA  h8-~mx世-~wq*Mu4Ԑ AE %KBgCM!p2{oE,ʛ]5%an;Zt4&( APHife1I=lSٰ'=Iu"Kﻏu'Wsrk=4~g6,x-k`Cd-Jv"SK+,9oO¬=};=L~t? aT ,*t!Kߤ٤2HfKtYm2Hyv s,3HNzN_8uw9n ױ'_su@:v{=kkr sBOH).^ʴN{["۳ @]ua "yQ"̡փCȜOtktaȭV]P02b fmHO/|_>QYkUe@M%fWMv]N}&!5Y#ar`Eb7Y+mLB4OBp1K'+A6q͜+jRnd.M$=Lz0wYԺ>U3c@®3Xv^Fo7@SNn%ƼԸ5@a[{9g\Ŋi3,K Tb}X=Wyߝ5ʬ;Z ф4Y_%={9 Q%dhzzEvtv?w`'>}ږwmnr/ETβ$I$Iv.ֲ+}xII~#^ԟ|?kT8|Ғ | :іU& ߱49CہJoV/o{\ҹ|z=?\ "Bmg^/h~pHӶ6dPeVsZS&eJ֝2>K~Ghc)d1X˷cIP0I3읙A=9}eze sO|3|<1e2uh>2ɫ i{x{<N}/UחZμ=_XC rfjAn;{\*iYsf-Ef3=r i˪˵'P[n=o5!Kޯެ `Sڒw->:Zԑwӱ*N^ ""h$(a) p5T-%Ko B.nZPкr7S$P2CQꝜPf=7e_yqp컕-v1aJ7QWXs5^)5!,`m}3$rSub嘁v&oZ.ޛR6f!yA&˺Ah>%21}HWV8]o[R_T;\QO= `C0$T\~D}(U2T:j.PV~#UnxpS=]^ϰ2FfŬJXD4ݦQء/OoC){&ڛhTؐCd_*zgSC8̚K/#g̣4؝V[h.ea LڸI%9 HO2O~_$rH^QڪiD@b%+H.+INrJ,BQM|}6nwŚlQHʤDH\W!@("`9)"@e@Ͻ^XK}|1r$әGaI+HaY|x]*;uBlt*$}GoY87{PV!˻6Eא4+ϒ>3wDj6y#)I$ILx\?SoAe薏d!JNN&!BAWYܕMa,ACU2z1Y%^TIZ*8dM"^֜u(Qr*90t;VȨNNV9%=˳֒^ !6@d٥F‘nr{b5gT吾6Z>LzŀTȠZJ(i,_FVp0mhY+ YYQAՉE/!VH`=M T8 Xp. 9+HZ&!'HT6g} A*ъ%BjTae$z7]'-CR'J9T*xDhq{n\NqM=ŽZp* X3SeQ*Xo K.)oEuAr"Q"AiTUY=7A]훇ĥ6RC?D7&%) tI*$aIWڐR#? w*>!XQfػ*č߹%wJBݾQw?{EA,EA誨6\슈i!@H'{g~4Є,Ώ^MH\y%mſ4x6pz3"Z+m6&;vb:r'@'NĆ^)u+#/#ɖH^1ُ~Ψ}6o͇cbxy}VmYj3|Ʋa?)m$=a 0!tW+%xڛR\G?uo~H: ǰ 4> *%[`axL̠@ڛ} Ca0pp延|躈=>7ύ6NeT{3JY{e١su׼z0x?/sz>44zC-8W@AۼuDo},v )`=AQ©`/b6Ka H\(S[q FHàq޺,?OT́+2[Ip;q7ZHa ݩ+i{ tlgxO-O㵭oC$k4R &G1f-ޅӅ_f {#~dyOV<7n&~QgB[Qć_ l@>٤*eب,Xk?|Gsuu#yڙ9XP>2J;Y5ox8vp~ޟY;{GO? w6az?ԩ)H!D }o',ml43gbVӋ1s|fXĄ);d=OyV>yz/'~X'2opƽr=q~dn[/d1)wa8&TQ29 Wzܛ6X%|?o" o&gw yȵ~'d H"z6mk693vُѢJV4{ɶ 7=9_burrޢʣ_uiax'- ,ð[Ƒ3j'Kf;IY<]K@[&XuK@1M湶VMH[6{Ϛ0Ԫ~mӎe:cY6Ԁa1oanMP]ڏgVu3toF-ˆaa6 ì[7+ # IDATyn0޲~S!Έ 3u UaQPuhv_ ?{朄=o'-"C(Ϋt8e;#42эsϷ--N,+9͉يlPm+ty-W׏{qd0<} Ï_mx"?pv$eLr+L,.Z? H{b`\f۵nBޚD5m9i2&s|:l\D,;'tJ (7ül 1e/q/aQo?*x)݈ %u٫}")fRP^N>Ob"Z/`+>g6an z]G5`am3+qUD<{&2l@Co\p7Z8XU0zΫ㩈,&aR-`k[50<~X_j~ {[2&&'֎HzzoOݲxoz)wYU'~ өZNZ2no/wc3eiN_'9N,vsk[~ӨZ=}YLu͒lG=z[ͺ.? &>ݷeW?3?P/ȭly{RеU׆jo 5fۼWf,`ggS[uac/|'$ٔym6\Xe'DM>[m2X%EDDDDDDDD丸\kJWE~KFEiFdꙤ5s~:z䳇 gkfzg}O/k\Q ݟae Vw,;>+OݿP\oY,ܻQ5zul]֨\?v51<ճ{zB~r7BDDDDDDDDDD$4XDW2#ᨛ1[VetQϮIMlkyd?&>YG=.t Z|Wϰrܴe婛7˪7:joQM _6e?jg?_#3kcaNWiDDDDDDDDDDD* EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`kl"DDDDDDDDDDħ(q1/Nw˦tED<ğ&-HkG?`ʆlkџ 򥋙hC93 \` ǿ_53ufsm46OWvОg1w_w |qWu(ֿt3eo(e.[nk_o;_d=EՙklzyD_ͷ(I66mc2~:."ۯk6 >%g B%Yl}m<{5_)®Sz}HN2/H~; x9RN y4&Onú!ڧAU,˦udZ w0ogҺx.:gw$(dw b֗1kx&}Apk3JLU&͛ͫkcm.{ovAl $F8=ȫO]IN׼:%>MhxSl5|_7﹛F2_wl-NB\?bʌSwsg =MnFvak{~%colُ{,IcT+zߕs1h:]T쀹s+_0>#,lo12t 3{wɿo % qi_Wu({'UAs[xuaKt9`q w'ViId^m-f2 J-,GoϢͧ80)r 3ަehozݏs?Bx$6?fw$x"GEwiTǞAܹ賓k7#+OKOm6zsSxjkର5Lw{ZR_K+3q9O"]˃wUwnPz'g y.BɮYׇSNShs!?sRs)s>Dz Y9>5.r /tFAn oNppP?6ݵˁ3pb,̏ :9v-0{g; 7JJ9IkU}dπ^x,;V6Z<±f>Ib?4di0*cl83 YeD=3M%]ISn`l"J-KR'fKCck~v~ٌya&E{gǀ?&'[PpdCZɘ|7 }em.)Qӓ҄㝴 !n |ceiO=@V nU87W'fC)k^ f}eF%T3Z׏]'9x"(oLicm~!{_BYtKܫ'gf~#̧xâqm f?)[R^H)VknM%Yvn^4MЧwNk(0j6Ki4PByΣ#j5k$~װs :/hϞ˻R@sıy:Kk1MZ\Ŏy7DBqn!qbK+ɹoJR$kecaw=/fN {Uʾ?w.ʰN}]6m0("/oQPs${-yiD=)FXҦĽ%?%jW/~lLAw5vDGD/O~ |< O{ƍC.#d80s,&ԁ R02O_hك3<L|f*֚y_Q~;!`D:.ɀdLn}hZ7sj䵬~>p=% h:>KGCc"^q?{/ÑM.y h?_VŤV#ڥ&#M+w&AUیݿ3K$Iț07XW( ފ{&>ä;Gi N%VuOY}f.)lGk-&DdQL  .x($zxZ4ҷwsl&GK({=;_c4ލ0nÁSUٱJ֐;ZPu@v҅@>A%، s"0KՔ774>Wueϔk)WUEsiXzV61_"}Ey}H=U>َ~~ s%)3sm{Qx!vmA\qf3EEyzp*>n`'^?w.~uc<1ヸ09|N<~B%vg$ᖾgF V[ңjq}8P.6/.=g]VCzSgߒ\lc3.Ʌ熲rɩv˸ry~n7:${U_ˎE!cOut:sW3k*e{<{_%T[1ɼjr,_$ {h=$m.Z}?mܭ94w '|Gu4'EȚr<ΛI!`w"c]ٟQ “$t{Q$5upX8y6[꟟\Ew#Wk&mP,^'-ص#~м1"qor|$W]Lsh>rlܸ՞";VI>N`Ԃϐ,#{BA7rt.F"ϣ^ 9$$Ě}7'΋puCl".H<&Vd>|Ga1$'G qvNwKwz5-5RU`DPtR3BO>r%97t!lZ41ۜINTSݾ%qhyX;Ux?ˈ}z 1{]/ĦN"LrZzCʱSvHrϠTl2=IIChae ?ShF6r#Șؗ[k$O2؊q7PƁi?cgS/Qѧ;O` TIɤ\{%d0ܞ"Sv7kJ`d}OdЛUfX(q|IdWL7=Xa9ivF<Ǿ+&nylj^hp2FαGȫ>n[ lvͽ&Sp{'t͠ՠu۩8b#PrߝHs /#y-}81h}gSp|1aKioYsz*j~ z^$]\^}1 6*⻥qM_ZsY[ <(<]$5s1Fd78tFtbO?N6b"qv\n'7- GdEɫh x` ه>LVYOS֛kzL":2}jC 6l@Avd>8sr$ ڰ΢8'qKKf< 4BvCAHXE6q.<|V'Kb{شJ$ 7`S6cD)I pӻV=/[P0^wg`j[G"W hюr){ ]Mk rip^ooT;4E)3r2fs74BgN"{8/%,NeIXmoze;rɫ~K(t8Y h*z/`O>·yv6ݼ-MJ6鼶zj|T/ӊcʹɳ^pIed*oVsn'1컱/++?ꖜ1m*.ěM؈ ,'ܜ"¢±3KuEQJnzEQaxnob@_pߌrv4k#IYܸSX.{ayeP#~{Aed6ltCEt>YG!lIv GÀʶIؓ?t¶̂W&sdrkYi`lI 0B)֊ʨ$bldg6ZXs1/ƏzY1Tc4>&Z AHV+6b&zo wH JJoÎl!x`A8ȚOr֍}I9fH0V`w ~~{G\DE 8)W-cjefEA v}kLwnB$Lvc*Uu/4j;ǎwnxT٠Hx }*UX̦8WF[Kn2!P +iϡ S\xZc "o˥z;AL1Ta""""""""'Y]ƚsvٿ.,[hc8h'*$›kkK`. ܰ!C[TAU( % ?z 8pt /ظhl{G UT>SHAQ&&dζꋢQ[شW6c؋-BMzUOQDN]iX|E_li?&5QK> ~nLcmsr[`c;φu%ըIu'∻څq]oؿE76&F΃ᩛ5eሚ1[IWsہx[{H[M&jRn؎$I= {qUDg9!2bJ[ة a1ziTe+ t ewwe]ȹ#{k+8t`ذj6ηaϞ5׹II\B[OMOszބ a4h^C>Nx9W\FN7!~uۄa#G ( <?gu}x?7 (&{HĄaKm P_ooG+Gھg۵A?Ft1D=; Wސ B)<$=Cږ>?5c^}8P쏏˖̾#י_FaV`MO^zއ-"~\c{NR}ȿuMEexBx:`dvGieAy}L<c-(/H)R .Qlaq`\HqvqW?Ӝkz}*j`Vx{2\{h8󭟼|{ߎ͝J$mw O>CODѴ]]|^0-,j {o$qCD("Us;JZ:ⅱ$mph=7VO""""""""Ræ*Qebu W 9k$,Ώ^MH\y%m5n<ޞ1FpC;;9W3Vlv9zbsN[O$I7q?$kzfZ\9K;pFbV[,]aϙçӼ ׎֗30mw뢿qm'o'չ &^*)IF}?vϼcݩfW=IlR{CnOQ ӡit"q#Ni'g8>o(o dzsFEvƾgO)]DƁхO`@6?¸^yXv]YL⸧_JSf6N> r?٥6-VAϡwct]f O([ '^7Rۇ8OSeU^Pą'<eDG X؊K0BvJnz!'uCy{Hڞܗn`L|XBp+H~-wHݟf|^r#7B\dy};߉P'2opƽr=q~dn[/d1/ w@ csLnߞHeyoZG2lp,^|>#9+"[b u' Uf孰c(oigIVz_ԜIQw>6V՞< kkpԿZ\Uײ8TXJs(BYzяJp>cQx29͉يlPm+ty-M=YX}F5TEU? LS)7#(00j.j[D,Mal.(±g1/m9o&ᄑxZl۞i?gg#l#gٌ<""&8g19{shYTcߌ3?'pKZ.M91J8}w욷H6o`4/cl. vyW 'elr\`Y?˂O_Y!$Ͻgg}`r[_/ 2 O@9sf`0]M l?b?4{/߭(e/6}Oj7,b”`2<}+<=]!?]G^.ϥ/+W{{ǎݛ(ܱ*v 4뛬܎LInE_g٨~^GEsO|T=:S#߾+s1yZ #In"7kl [S`T^} {g[i/ w9ie#xfW>Nv>XY2RN =o1 ]d4zZI+oxv95g3«(mer2 Xvа3 VsH9 BS4czOϞ WPfn9f%c7iR>ecO7m?EI=/ gV?0sP:q\Cg*yt.9 p=Zy{U‰n!q_hލ*JHn\,jZM:ƪ꽱7>¶* $x, LF#=Ξw*J3v,#3*)J,_>S;_wP2=L[6WZґ/2N:?iN.݈f}&=`O!%}@/!ljvŘLYZ/L@3us`[YFo@F~?8eϜQ@ed?I-L7wy")\MgX*BU*²L.kcx!Q3IK{՝w06ݚFK_:*2{G;DX$OiMs(2"""""""p)]+2ȹ!=DF+v)3dQ<6\=y1<{^BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">BP,"""""""""""""#"""""""""""""">Bp ruDDDDDDDDDDDDDD@3EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG( EDDDDDDDDDDDDD|`XDDDDDDDDDDDDDG(gNۺ/D$Dْeβt»茨mAAEE=ޠ2r9mɖDQ$q C/)'D\_\D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D!$BH  0@"`D(8hm}108[wDXc 0?r9^Zz -RooTϿzfM1>w$BH  0@"`DpeYw?t  n߉PAsGovz:9'@>Rs1;9'@*j\u眏_m驈xݷNJR{殭L -`~nooÜ/i:;!$BH  0qǻƮ^}Ư]\|r2 p]ɧO^mb,.̿q#c1T~GwFOŷ>frqE.S.7T]')x1vvvbc}P}cMF_9J/_X^^zcFz糳_B0Nx<5}!VWfo܌Ju8>yt\]Y .zã.H /RloQjlmmƋ7zN ]\ή(J.LrY Uczٞk(qz#ڊQ ̓1:~iտ)GcR +sl}…X^^y,ng8%3So݋W""|9ڊ=fƓ?+KW.+ˑҥޘ|:s?;E pV|Ͻ`L;lFNNLOw]ɉ'G>G?kuM1x69k-h61;="q1>~5,k=ۣ/^N-p0@"`D!$BhQZkr9k7o偁C=fR˗~_|RϷnǾoN Т֢Y ZH}s<0g^VZkkjxccǺoN Тfl[[VllxhDuَ SۉmߜgL[7oݎ>Ŷbܸ};pЉ)88=ãcqBO4xݷGFcjxz;۱1?2""Jqڍx|&]`tFħ\W0NQ{{GX^^zwtv?|ܾi|/իŨ|?Y;78ez{RGǮēcc^czf.`8 =}žr&`8ՕX]Yv!$BH |u?;rMqq͘{A/cqa>뱫nD:=/~Et_艎XYYB-~qhFlnnDDD7oݎ bcc=jZwtk1:6Nl^|y X\X`-Ow }Y3֚8ѱ+1_^EV3>35K^Ãh5usc#~|`Ņxg7F#^xjLO#:f>xs/^KFsbcCXuiَ/G! p<ʚƞ5x<8xxk133},kOO=QHLb8Ǿ߃(_~R{{\v#""^.]c[i܅1iSOߙD_93w<'ίh^C2zJ5^<;zKzC:De;α_T{vcfzꍏY__?!߽^:# "fzOh/ƏwdZL=|=7n}ssG6ETgOFwXtvuڪNLvo@wD!$B`Ey>^[>(J',i_*|}XqwΫ7{c-eɿήh;kj-vj;QգVE^բVst D>'eY36c-,m-9:v%&Nhz0;QiP5nݾsVW""bss#jSvlons^ٌٙF|v ~s-xELei|6to__{y G~ΝX[[;НYxPzlmmsyqaaWbo^c;Ve}}-Fիhސ`d="+N}8t,kkjr"FFǽI@:#◍4P,.̟w\#==ea^ZZb| p ,~SϷr1PJz{ƥިTDzq}֞qwFvoR3}в,?[jmNW@C  0@"`D!p$n3J%{oܺ]'\'>|.˲gDz7n}tR|Ϸnprz֌3|.hRƋ/h|r}wQ(k7?"" B|/O*wvS69$ƽAH^E\3OB}l䨡јFc>GǮӉGP޾rx>^/_[bērF#66֏Utvvycc=vj;s7[ɣ{7\V,FOŘ{bxo__ll:2Ϟc,ѱxw^,ƅ Ǿ筭X޾?y?z[ۻͳ1.8fN#15txuxtR*3񅶶ةբ.(ڣov37dzɧQ=6?7Z}v,/-E7;\ 5OwQ,ciծX\Zv,==?xtJL>}kvvP,wvtObDDrY Qvcg{V/#_i황gQG.yCXC#FǮē]c\.*133}l{7*럻#˲X]Y}5fL>݉7oFrD4O'ԳɈx|6/wʍvJ%򹟯k7Tl?3|z=^>J՛li:8[,?[jmN;`D 0@"`D8QZkrQ<o<R)~qw>:YO?;>k,>Ysg/1^t)Fǯxs [ff٩^E^?;;Q۩sjDdݲ׭RƋ/h#>`Nt֌} h3i;_jvloniz;9$FZ.B}x|8%?8Q;'_?ތǏ_[]#ÃX޾fՑ~H}ג |pV3:M>hK]R]]8}Y%p:#◍IWB>{GKV̵Oy<`N]3DzvW(JGZctJ\t@s^~n#߼>/?O\|r2o8ֳfkj'3St͍X\\8 ]o41Xz *"'66֣^(2T[;;W5|v6QrD8άGY3#׈W8ѽ{^-.ڑ{k{3?zg|zY ē @6kČ}m)t5z_\ή#Gs `ZΝf.lk\wDO#wryMNLs0-؛~SGhWո?KQVtޥb{\vs߭De}wr%_wy}{"X,E>wh+w6#FdYhkG{31;3}^__?w0w(_~s/?wXtvujD[ӗch} 0@"`Dh)B[|7oEoo{E.qw?,,;ҥr>I]qG!r676LJbqqЏ7TdVV#"s`q_Egg׮ήկv=r߭w2z8}o29$Fqt*`5=3wzY fb˝q0kZ;wk367֣\`1?7Z}XV=s棳+J҉oyy)677Z\s߇YMMM>z$\|!k,_Gɉ= -wǹr\=ܷ]jX\ʹy$굛Q*+굛_]],ˢDWV;ϒxlmmhۮLO@HH~{wb{ݷ-yǵbk~ﺖi6sp0-#n(zF kF3NŎ{zd9$ҙggcjz<}3~# 5Zf4ίmvxӥ/^S*csc3Zgg`&j5>0qx>Z''qrtt=lq{فs600QpLj13;׵^}E ,wv|AZ+}TbR9^ڥ[[]_ѵ?~# ?,<|v?^|>ÅNlWWbЕ[[jK @}xWDea+ uǟdcXJoWU{ww7r\ .\ctt,έULB ]hG$12<##$I4>U^]Y\*c|b⃵[\Lfg3Imc06>c㱺=YG #Ib^J~I7xp` `)!H 0@JRB `RѓH䧟?xCµ52<*ޙ?|_";rND @jkqЈ$FG.]lFMwB @ji]{/+cnauNNOfc<(D @QۗshDVm_ɑCvHӓݍkemM*W^d]r9'&z:"t`n#/{?8o_~kըתֿWrx %g_y %)!H 0@JRB @j9JżO_][[,.ζat`{k+ZV*jb{) IDATkuWWba{rz{1Y[ @_,,ѡb1T;;buvww#PZʳ$}H$ {K b`ܽt%VWVb~÷[Vl݈Rowc/Ixi_EOFcOέ"ɜw^]̀Cc/{bH  ` %)!j$I,wnL<~LPYX߼1T(txWq{I }uvo77bT= Olq{فsvgQp}6Octlú&`#Tέ׫xs9o_00<|8::NNObow7&wFGbppFW[_Ry6210Fc?$ᑟJrOL܈ZVl݈R#ۗonTt  ɳ/ ` %ӥR̔gG6O#IrFcQYX$O#\[Iģ'==5 ?'pd335jWi͍. #،ٙ3x f*Q6=Y /:Ϣ4ӝ๛bv167j 'btt,nlZV*q|r?|jtvyn>kk`p* {jb{kv+ӭ|> 0jP\.zJT/@6&ŎKjWckgOVX]y}-{7$I \`ܽr9'&r/ݨ=2<IDc k˯uW߾g߿?qtqW_qи|~M<{n>$9Ac?R{zu yEH 0@JRB `p$GO ?.bLg_0>+#"f*Qݳ/ꛕ֎57=Yzk%0LZw;;]{|r?|+or1::[o7{vo8>:Z5]]_J=o{z^+ gook[Vlomu\e!W˳uΏQ|>WV7?8w}q{;""nONLDDυwgFG$@6':;=T(D.w=q;οcwXwvWWVxo˯:Oaeun'gJrOLttm>7w-]yb=g3*MGVݽ`7$I ՙ觾g_p$ƿs|.ϭ4codNέ׫QYXwݵ'1'wnܟN~d9nNp NN o;FOc/{}_ `)!H 0@JRB 1]*Ly$I<~̯iIģ'׽wב/U#ˑ$ap#d335R*csc3ZggWZuڌiJk6:;1]*;}N 03Wzggkc`` &ncsv嵏뮮Kبd1cpnr1::[o7|TֺR^Ac}:T>&Ub}Mƃq{r.7Q\xv#;}\B!r\J@6'b{k맵㣣xW箭,,חjw[ٌ혚.FuVWV[ykU_Օ] {KyH$IFβVTcFc?$%Ϟh7dN>\lG$I;ڑ|>$9x4coR}r],b$I~q#8[ooD''Q{ .n{E ` %b_"wtmT%Cc`HVfk[-Cc`H0=9=#Ccɳ/~_?,2lI0|CH) ` %)!H 0@JRB ` %)!H 0@JRB ` %)!Hrokf^IENDB`mpire-2.10.2/docs/usage/mpire_dashboard_error.png000066400000000000000000004043051461637447300220330ustar00rootroot00000000000000PNG  IHDRO$&zTXtRaw profile type exifxՙYr%+DYE/)z}Rigo-L%$C q']J/{qp3w~NO.\ o05}5 =?ؾ Kz _4ٖ/Տ[>/-aN!yc7 ýoI5NêW߅o?9%gcσ|y]9.~~>_{jnIkSo[wt< ʯp_iU\GC #Uw\cTaZGMlpbM=pw: Wg x#:_|~;9!1c+-XyξCy#~|1&<(̍ ?!WlYDGZ5&bna1!_BPkر@XẻlEwj}1#d2F܈!IҤ("Z FM5Wjjk6z p^z>F=0ƌ3ڛ #5ٱ E͔ZsZȏx3*'eMjJ13Bryp&Eo-{m X0V]ٝ۾&$ *Cm7z p|w6ʆX*`aFX5HiPMI^^ ī>L2l hs wn= PڻV!.艼.e,Y8 eM:zm"r8 !%P='pʟĨAqDFtndM3;T/|0G Jx/Q= ]H84' L'K):I 1 `|Pd;k CD`~qY|#{v}&yZ'ey.6ʠ8p R+}>ggP3 oQf.=Z"EKldoRGoE jE]ޱX.1!R'`'YrjUԑj )uA";lٌYy/;beYN$;`@' ~:ځI N>WH_+J>ނם6޹{Gy f^cg*U}pnڞF T[aqSYձp|Q!~i][Qd86 l0&P02c㪛@+K-Jv-(Z#|H3(Й2۴xFj5vSe^M0 ePM,yR LZ,YlRE.g#p=zf:ɷ2 nfEbF$tWv?5SC!3)| r,S6j@`e8-G"q笒6a#zQ&Vd8pB1z(چQIv'`hBgXl%$ 0kblr $F2rzCΤ Ijv%YQm&J财& hMt@z%lGzGsr+5| CD;28h.Mjz81 Mj V^W IʤZv^>S:.xӄh4%[Tt$fw T#?ja)E[ ,XxI7VwIH7zj#=2[ۜ5fJ̅x9M應'F [֌2~37Q$#t73*&ؚŹzceDŽi`1HC"Ō#N|ݐ1;8K`.B8Na;D+sd'K\dZ_OKq(.HkD*?jՎ9>H1/`NkHy_;NDUƉI6Ztߕ`P# M jLyE4RYE~f1˥!v)XiAJS1Qfenۙm + IEVCW#&ëĻAii("iT"ogu#iOk{ԓMэEC?VTH}jʓ"Ic2"Up󲓸uT  {vq8Uo B?ʫC4 _@0 ayPQeD'z5h~Hd>'oDE ePxjggT j5іx iTXtXML:com.adobe.xmp *;iCCPICC profile(}=H@_S" vqP,_8jP! :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽjiV(鶙tfE F2YIJwQܟ#f-Df6:Ԧmp'tAG+qλ,̰J|+M F%c@| ]VqcǩgJoKU`JC=uCSɐMٕ4\x?o}@ת[}@JpZkVr?עbKGDC pHYs  tIME6) IDATxw|gf7K#$A ,^^E\ `$C'$$3l!4ۅ|6ٝ3gwsѢ]g[+.g                                  p Tzud#Ѷmy^%&ח% /uȐ!N0uHe4KVzuLJ~dP/U%pAA7͗9`̩}^L A A A A bV4(HY+/λ -0O?ƍP-.j޶m뵔uFR;C^p '?5sN\ฯx]ᗴe)3+KiJ:hs&mݺEe]p_*TIIIz9+}o@Ur T yVG1 CnKnw lٲjذXG%%%i QhHJ.:Kkta}3f\At˖-ZsDD>7AGP|UQ3[I.yB 骈ߵcTs>o.xjܸ*Wxrzz87^q\n bJjҬ4\hHAde]p^Yz[3VN-=z]ò<%'к4/j2/"¹]pRRJ.-I*SU}{^<6k ox ذq/SMv;eו+WRx?lX?..N/Rdd{KJ.kQ /Ӳ:&3 x*KJ$jJѭթ,]M@ܡ)VZoظ/Y+k)ڴmwYӍ7j„ ZhY?-ZBQKVhί7?qUZuЖ;\5-[Zzt*IjѼ&NYm_ŊWzu%IiӴef5o/Yg^ մ^ +/7*%IR*g t֬WԶU3[Dd:whvTJN>Ki5LTԶUs/ZD ǒ4sn6n*IQ*+jGnM6[O8r?WD(YRjպi[loy͚xzgffh|a*^eO333v8$8ؿ?Ӗm;^$~S_УVߨqtۍ}uMiCw57?úwQBBBАA:~9ZT(4TiizZnl۩jמ}oW|+jm睦eo%2+| j^uUӷ_+VP A0̜T[n nːqsjuˮlf:Wl+qʩ7ĶmvYW^%+CB%j*$I:z:`*Z8932_麮Զesu5?jaÇCX$e6j_*Wwj_LTpP(*+UVl$o\\֬ C͚7v:U۶mC1_$Ēe=?|?VVMϿ|uwRFjժSJ,!˖N?۷iڷ?p]4M$[\i`^t0 _l596lk' &}lCe5e<9a}|\o;-uktZ$hJ9y?NLOs"ǩ\d:x舎M?,4UF5uF}﮸=g~C6"&:Z}$5iL?^oVZÇtС?m PpP -J*I&jݦ\.d-Z]nB~<߯/G~~M㺃tۿOoSP<'O- E04ছյkWܹC ױc,Kjּ{y͙=GSL~w  ;-RN9esj[Z <OSc7͹ao7O` `8wyk {-˒mYl+;;VI =pPz\zTJ6j$BիV/SgIvUhk8$hL:YFWӡ#qWǒvCW#Hiڹk+a*Y\Nl \Haկ@7o*psYoi=YYHeee^6233SQQ 4steff: /1oS0y=ߠ3ngs[J/RÆ uϐyFfnAA ӌr6O`7 ۏp@ݼ5yWy%_q~-˒izd˲|132TdcNl.iq`URISZ—%$&)zzMA:p谮WGmZ6Y(+գ+cԣk'ٶIdCUX^'M)rL=pZx(==CwvuD|1ٽG5j֐$h٪4MhB/􉉉sRR?:l٢ t:ߪi׾uVeu:5UkVhʤIx/)%%E}qIwyWS&O=JժAP5yDm;sѺ[eֿ|CIII֭Y%KRbQM>Mk׬/>Դ)ST^=տ*yYZƍSXP6iMkהuUREmݺEƏ#?ח_~ǟxRcb(-۝ήqkֺ{O08O١/4on-`ȝ7r`ki:p,ے.,7`;V0tl[i3==CI5y-YoBEVd4aG)LiЦ}w_}>=k-rw}}:v꤇}To8_V7j -cǎo?ovخHweurqfLIQUu=X1͛;'[4qJNJRWҥK4}nFOiI۷u۶>uLKn+&pN3? h( 78g3 W!_|;ok7._+ks̮ \wcϾ|{^MXY4k^TP}1o]Z7tLPhP5lHׯEV1V!?vN:f+L222tI%&&,W;vl/Ң|-2 SjawiBfJOKӜٳeYoۦ3YaW֍7ޤf͚kb__-[M:z/^ɓ&vX z/Gżx"-\+x)ݯկפH6mڤB@@e͙k>!!A۴UFp5U(,L+WRۭ;VLt$i֭J;>l۶ 4d.\.L+^;`)o7n[o>FAog?y5ldm2-e#eKi_Ӕi2Lw\ SRRsvիW_ԼEˀvi|Xf 7+ITeytǠ$˥wޥL͕`׮8O/Zz6jT)mټI-[ҒŋV8L F_g}˲~:5i4`yjת]K.K׭7,IjպJ,Ǔ%Iϩ'URauQ܎deif*]>~:>,;HOWX0IyCX3?P_kܶ5l۲&/,έ2\<@n-6ev@sʷL+I#7Vpp233%IEpXaIҖ͛v:o/]D+UQ.%j^;-AΝOԵ[w]߫nWǎ#b *Q""" 7'%%%ɓ'Ux|r)\$i^K|M۲|5VK*ԮiJHHHbڶuq 啔0缌6sTÕmTWٷfv9 o7Oφ`_8glM>cچ ]m2w}y#x5rFڬ-4j &ԫ}5J].>bdd-Allbcc ^uhЧR JKOS&M8Rk֜spHHE ԀaykЦgK>S8quJ$I %DɒRΙ'$D-иc%j[& sM۾go؁m:5uk ۶ܳ=ʝinՖ<ߗ&8餍I;͛7lR/슈$ yy12%[n[lC5k뼹 -0_WkVVVUN=ZԪ];qWi\]dۖJ*o+WVȺľ^Fp4^eʔQ"ZƆa}=ؿ_aAuJ,%+yُZ^Y^۶snq7qo>%_k^nxyyy[߼^y^y<.w K+6hML"ކM,IZn2y8Ι={oA+Wiǎ۷G4h֮]…^K'4j uIIIIw|nhbuVʗӘѣϹܔ'xbq iݪ\nW11/֖-@*UJѫVckjҴ*V(Ij׾j֬r?{Ԙѣdڲ.%4e!#]l@ uluv`?w Q`0p98,<ܦč 6z5M;|2o?ꘘ f5MS-\=JIwVVMjݦbccxe9rD>:umٴI۶nՉuL lrj5kjٚ>m$dSגe4L_}ŒYI2ksCڳ6r*h;Ϣuvsʞ޲lٖ%X ,IVhQf\mۗԝSrxX)PXpI~QdD+&Ir\z$ֽ?gl<8ݦmK.yڐu]GO?pٿ~#q&j֬vbŋ+8(HiJH׶m5fWJNJOx5ͳj)ϛj?ܚ!Y׾ 8oSf}vFGc:8RPPp`cNf:ՑԮ}{ݳ3Nܚ5jcNZ0ږS>y;_QmY֭]uk^9d״5y fj+ρm@7'4;Mpq˷^R~-VvlY^I PpݫuȰ ٹ{/Uu;5WDs^11jݦ5j֯߰^'S+҉RSO%ILuYzumdei޽m-u9<g !!!!!!!!!!>t IDAT!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!ܗ ,Ϋp<)^?7;&!!!!!!!!!!!!!!!,`Op T\lrjlRf+Վ mWfӷwU7TAkŲ(p[@U-n&?ܧ.N\/iRqWZ4G ~hSù*Zpf}RY74v 2%e賅QZ6BxQ`V=n@KjѴo3ת_x\ڱ}>-J Ƶ1`<-?I>yV#g_ U[_1o,<󗍗6q糡;N.}%;%dPh^Uu˕g\驱 ?xg-~Ut7$}gPrOw|ȫEMKмh|%aj0y{G{+"Z? \ VjzTI ubћF@>RO缦XSC :}fwhy"+~ԿGnWRXaֵjԴB#Ӏc5LsR~H#TUԠR$S ǣ4=.M$wODI6TcU'n;'nӷcR:T]jGĝzѾjS%D/|1ɲ{4R墆vj)qJyΏ,^YdhJ$CE?o1++5Z-zb e+b髺E*M}VMۤ ڨ5hV|i=ح*uv?z?&RW~(-zf:+J%yb4cg"2VcqiT"ozӵkWzYִӬTj=TFeWjOޮUJԦ_Ѯ3??:+ Co*x--*KCzF\nhމ:*SO$TslǞu[Ne܂o55ע̄75j FE jjZ9~R{ ѵ?R'K7S%Qyӣu_Գd+H ;Vʡj2c mt^z.}t~ФJ2\_&lP榅3sJd*zjV*];ЎBR/C?қ}0kFd&ҡί5ъVfP#T'6?UT|]y/0J]#4t@ Uj4bb2d* )rg=6nPX7T#vRBsN1rdh=զp-ݢ^ç[G7\>:~.7}&|nϒBl3`W[TL}(:RFnY}zU;A>Y7ܯ{K>i̘Kh?EcWژXoI-!WhY5jRC [SR~&jHg]Eϛ#6K?NިnKSdV_Mm>\on.F!4}2m._#F V۠X}Zi5#W .zzJ^xc8VU{-o^J j7RË ̲jߩ6OQU?b:phv6R'nS-2%Vc-J/uT>|fՄs#iS4s~PcnBzC7Tú}/.T5ޭqx)UkGajzwU%OT?5Q!kjn7h*[]yYUZ 4iJ<Ǘ*Yo'KǢhiZ<1J߽@=XCK&j٭EG4ۏdLJ\bF(MW6fT>׵SfzAj[|?A+ uUn^*OțK݆ަVQvtuφz=*޸=Z}s},N߯c&iUC>r-eȬrrBfu0SԄmջ[iB IhePַ빡9ux2WF :JI*_[ pNzWR{f,ٯ %mXYv(|F򖬣Sʔ2n Ck5SZ1-^Fuz*H/:Y3%\r%t=k_ _מϵrp1uN٠,]BLR%TJau24neX[zm%[F]̪dztܞ%л=^`,SQdӚ(]A[K٧ԣc9EteZHʏmGfKf~TLd)b/^._s,/!?ֱjQJjJpNmU?/'L<_Oj&*D|br<_=oec9k<*Tn^e4qW*<.u$Rʮ%YճkYY;Gk{cǞ|nj9[4`1WW7ߥI_X;նiC.pM&\^b8AN\D#A{jHxYu\v*CM<>Y,;V=tW^ԑc)vC$88Úi9{x4\=}E7}t͐uoF_Xg酻:ݍule۠oFfKeKش/LݮN5Qޣ$;㈖zE/۩ ~ѩzB^:SEiM|(>1˫ݻu|Zl2r(SGfmCR<˴~$^',߇%e2Ql_brW*F%hU]5*e XwoaV¦l+MyԜ/.&,9_[Χ ͫ*,$*n:[O>{4[˔S'3ʒ2=( juz^B{-KL!W;ͼ;|ePL*7)I)%ۣ,-)J>i&SK:lY))J$g~Ɩ SBTdL^n:zX=6ePp*^߽2W*L4Vң] Wk 3n_O=Wj'1Ag_n59W1Lyu1x$`eҌnԿjOʖd9og=s:}L MdddHr:9*Z\ [ h$*ZQl}%ɫ}=[ l| eﮪd:ZNfy<*7\ &0ػOS>/w׉X^\JsRűM@I=߷6zBݦG2 R֙z󁏕\jO$R7秩ֿ^Poո壴[X!i쫳5<-\;oTk)#χyYYNScG5԰fMu2Tf 1->(ωd \Y%̵JTF9=:h-ONehO YR Y8Uv핻HY7RjsÝz>D-|9:I \mԪ!M|ږkI2"CQJgde>Ea!0tFIU7&ixk^[m˖dY?f,WS5KeV em1K*)I%˲,Δcgu3-CqȧZ{=4^O2K vT-'%㶮9Yqk2Acϲ }FǏrWJn)!SU-1\ sΩ1>ⷯԹu5U|\q !)]>G/jؤk |R|J( yRe,ʖ./l-{?v>DVH-%[+0C:i^R h!][#CMJQt*)0UEO h^]sF9O7rVlҁ4w ŵUԭ\VdTpp!I+UP!w'j!C+#mR(EW]髇{Ocsw^WM7j6eQ<)~GkAj|UPuRɋӜ$-C> F5Ee(9s?]@[߯v).x w;y5{]g5:^([ >)W/x~#T7=篖\+u3UWf|;K-XKW_wZTGoU5~@{mӥ`d)$ݒ`!;#]鶩rnIyfݭ6kWkG/Ѫ:]#:!K}ϻ}uW5{q{ЯtːkTqȜK9{Mϓ QKp}/qT뒸ds9JH(U^QѺJ/ߪ,ۣ3lw=yny'T[2 -R^nMG$PEVrFDiɶ|r|7E+$z~8s\羦zZ_7 QdznG ֺkqJ:kNfjZ怖lH>OW}d3[e*KO+bkw鶑۲)SnzW??}"{E~@aSV OߣMը]Eݎkc1-[TljUvy6~цzMU?#=ߍ&g5hvF.^=]vNkSM*w5bFL(O4O\赧cdҸtUGlԏɒs^x''֍U5S;5 z2"n׋!hԫzo)9!޶:7Pؑeg>ϦqzwF%ZNִZv'4D=XCq1{qUUAZ1GV׶b9W{ƿ?!MkU'}yyޏN{aMmpm薪iZz{aU=3uҿ&j%]85?U_ϖ@X_~֊,jq_flTk5T]7]߱Žфw鍨x15v[g㣨6?3ɦ$Pik(#"*ME`+E)"TAz@*)μm7.vgΜgϙ7'?c'kŇj[fT'k9Lɋ4qkZzO]"VhO#ܳ,O5ShθzkW-{ mZ4T檝^+Eꢛ}^Ti:_ N}VϨXJ~~4]  -/k:ŌW*6[GS4~zu+g UlVWTiS[FkՊ'uEʢYN@ VGOt:gUkJ n//v6\{i8Z9ryʡyik9C1uԾUm[Y=;5n/2W;ʭiL ~¤ ? ?A ~0 `'O ?A ~W02:\H9T`~pyO`0 `'O ?A ~0 `'4աFMmdsiR[;ۭV͙h4c tQS?[rrmjԠ> `gGp_r\n8N-TU~Y ٶFʩK/nI#>*[ ,~Ya/P~\.(n;R?CU}|shuo[Y:=?o0#z==S-FNjRߌ=y_e_BUZw5N[F) V\&_5D3[XX[_K>:p@US#UNGX 0- IDAT"[)Mr>*2Q9^Y(뚮?{zz]5᭏Zcq uFN{O/VKQU'\d1UUVM~T˻k/u{㰢sW̥rQjsu3m'RoޠWQ5#C;Y}9.JI.利.L'|ZԺ~gLtj{Rܫog|-e2kU\+:б~Ե)3k_(U{Ds@F [ʵxK'U(N2coҫ gi@ǟn54@ hQ~;lS;C?+t2"ݟwUۣ}5gqkX[ >72tסVdHJ22ue:,%K[H=53<3:_}r^Z~XVQ'iVXܫuuSY(n׬=SPudUQͪe,s~'m쯄Ss(V<G+SEU y4W7U1NaP<vC߽cĿ*z3m^3E_5wg5Q#^vkoa(==\^#ZF*6v~*XP]_w~s.)%Н%i$Wx{ / Ej۴9.9E_gqgtRU`n~TDxN=;?[w+rTr(}zcgomqVÚnQfOk.'-׈˱OQҷѬHz_nόUIѥ_o\ETE}1Ym)8$=%MPrsSrJ.\fBr.~e(7U|%hSizz6N<\b.H_VuuQZ@rgɲsRU2FyROv(3ؔ{e]2\tl=}Ћ?(bE]U)+ɩ$X& tk}V׭VZr-CAAR[9E :nRg:{jUM[|5=jefsTX;FZL9:K; a.5U]T#'#>Ik+<ꐦ!~TM5kSvD޷ 5f TF\kNMNQҷz]Gc?]=&irU-n7(75)uH5Y^~>vgRqi((UNnS.#Хw']mY-6{g2[^%T.ce-Srܶ]>uTP[99e,cd#Ov%*q9N8Bܻӡ"Ձӕw탪XASbt7kWw;?4.@\RNvNsΉ4( d15,Mwx5dтSM^t:FΩ*m镧i #̲dYy^;ӾNWqBt~^4Yg)k;`v]_VFoYRe,xrgМIh=ڶ#8gHzir7y3*OTU5̢7cux~u \իPh|>S43/G9]qb.kye,zwOjTSakF+>v-c>wx;QΚU])^C;X80K5~P 9kɳexޞbk躵D9RSFV6QE5fkdY̘4'úڜɇ!EۦIr&;,%$ VnOc4=u^s}?OWϞ{{ԽϳL۟*Ih-<-˻_??t[9>r:{=^ɲ-o+ega(#7ߪUfUu)@r`JgZ}k^-G1 ?#W}2V7JTm&C n:e 7m=զK1-}2۫5M]Nr*0B^Eiq _pjm\Mgz㤲)c%KغzT`P5usj^=e,Uޢi}"/\UatQ%=J gW71Y2qrƥsR%UU^RuŒ?jIflcL)ob-6:[+< DܭA+ݺ\)<(Mvtbccw~92d^wx 1v#@sxYSBؘH:>پN;vܥ;wiǮCJ-ј'+>23suxJbTŊ^y~LC9th]>y_cP+P&`)zόDި-5Y7=1$]Vq5ꡉ0Yc^YRॷ{ l2Vv-eOGwsijmr> AXEA#8}>ݣ-j,|,|~xƘJݺD<֝-xf"=?PA2o\pXjpH=6EW=D3ذ<=]+Ot׈2$_#,:ƍ#Q:'7E}D (?#.}hvUrݺC'(ˡ,S 2"ӸG䋿(ocFV4PN\F9/j&/P6_O g @(gnޜ (',M)9igc=R\A!v14?vrj9m޲s ?'O ?A ~0 `'O ?,a/(R&Xz G?A ~0 `'O ?A ~0 `'O ?A ~0 `'O ?A ~0 `'ar\ Z0.ҨOՊ%ߝkɓ' zθKn(Ů9eT_7V;{3=qelA46h=)ɬV>+Kr[yCar$ƯZ++x} 0rv8QidJϕ<ǎ*!!Q^IFx6nMڢzQS s>x#fT }q%kVN=Z_o YR5U:rէNӬUɪYn0k>}I=Z+{iմrݫi s!0B\ +|ڝ׈;/S ynw_Ҵo3(ǂtqGuwȝ7k|麁tQtIO,A/&~:)6›ӣuٶ5_9qC[tӿj>ra?=?1H7\MuDm &F'Xծ=b|AuEzu{i@RЌzp`}G]z_>[#0#!֯Wmqnk$PCQre(3$R7;[MtuӯZ ]g۲%HRH#x.NC=&$58Q:cKۿkvҥ$ɥke-z꺚eY~Kv(Y+\1JMΏPq'KYX]mEe˲88}tn /}7:ar'׎@,R7ДգswM5RZ$94SC;ݸTVs{O(ߌVdQ.L\-NH%IFbt>85Nyed,;T_}i83]wPf)0$<ѵ3G붫uSU<߾_Nr8NpfQFxhz]|>  ]jdհ=7dne>*?F U#?׾2`+)6ԐRmI"+Olg)9խ/i'I2ǯԋC7ܲ(\kQ&aUUܠ~F4^CPlYNQ3=Q~! LXjXCNwjSS ɝ)ٲ<)M=yohu^ѵǭ»coo;]AN77*dDVTUջe\C oфW'k2e+/ףxU w)($DLd6TN9R6"[g1 ۭL/#Q~ѻSMӗst,}~ipjT5wdux8Y ):Dh2T>}8i F_MansMTE ȓq@?28cFڕ끆dʇ>{&-{ѽ)/]~Q!(sv(tm {Y-{n#R}T{C_Wy)׫ 4•{vu#ǦSI?*{&/SG'i)e}zmWKn}&1OuUA#+WATK@tm]yy|:Fk }ru~h3J7kcgve+-QMҪsTwi2>X 4,Ay5h]H.e@OV Q uex]]X1Fkm?r]vK{7>qARG7zh#Mۗ>z;T?ȩh?mV" IDATeS_VD[' o LoHWeJmώaT:q:e߰(JNl˰-QO+fqDSTgS'61l%y}B`:q[ `Zk1. ~ h0Q\m['L-:N]F WvQ>>o/Ń̿r#N^з< ֱ&v$y@]ܿ 5iMWV;I]qEP{']aSTtWe-Z.RPx; Ю.5$uRi۞#oDZZ J*8ymRaRv6:ԯ2WTY;Oo?TBc`zh4Tg&ћq/G}8bf0ծsAQ jد:V=gԠyIenHcӶfWZ;vVܒVgSU{AJʒ!YMulx5yrzL6Ǥo^9ܣZk[^(++b&GUHr*}:x5U9dl "}]RA"菪U8Qf5+wu*^WOjNIjYy9%'_4껌u]>{7^}w-4z&OP)kO1Pn{GEa75= 6BuԿ|=v[GzxԨaz:zDp&1^6Xʸs_X]9 1K2**0,\'Pv' KS#J]QIzAbChdYy:Uq;>p)rR ǀ@i.\V 8 $.=[4V!jQ%G#mٖ.ޯEAiF~m)9z ίtQhm[ OlI-7>8.ϱ|ک 07|=&*l$.hi,o~yE.Ηo9S "WY:oS=M+d5n}~SM#cm-PSQ7h^h$hUUvA`B?Zl!ϥw)1w*њV\3EI"L\+>eE˳r\`#uR#+-Y~gvR9y㴡Gƞ/To*hKjKFʩVOwʔC6㮦ܶ,V)vw j]v('*V,IozqygZڿkbIJӆ {ܦBq҇hz{-VסA[uXɳV)-i"'.CJ=9Xە05*M m#m"5l]{P>VYCuT*7"K1 Sy\;^kTd+ׁdet G@jqv5U , Tf?c3ߦ#ZOYbfh_W3wJR~f$m֚Ώ KǍtm Z6]JʕUsg-M;W2W[_RŒ.eށt9Z⑴E^B׿AVX_@R{${UMOpTEo) mlٖS`|cqbl-+~%kIFa*HX c*(.i +?H}kX>oaVA, ?I m%º9L٦gĴϽ ~VasEYEUP7KŅRD?pv;$;FfY6OT)}<:Xkߩ;cd+4yQ OQ`FE#d}N[&RapPOWFezuҁ;:)-t?HgmW@a>y}[u}Zk1QEHОRcCAwWm%f*2*\YII*Nu$)]Q2-Yky-{RȊl̇jkǩu5@Z2#Gj9$"vݛ2՞vCe*p(AfH,T^2}\IT۔{C=yI: 7G Hʑ$fi"uH ١r;!60<הm2Ķ>ilɶ͢QKm';W>SJ( C؂vlIV~F~̶nS=BifSB, b%buB\wԯ]Pۆ mp8G]="WGfvΒ ꝥkZNCa7*){82sWu8V$9z54$Cνm5]D2SƧ:~F#O^JPqjnK*nء(/C~,-)\Audn5_5)x`Se9?W$cfeؒ< 1{ڪ(8XlmzG(5}a볉'Z@y d[Fa׹hD -XOQ'3,߲fYr; e_(킲oF"˰ǖ-8f ]s:^vҬ$#oyDq)')$2뒥G^dM^OPM)uUinmBWwбu_i3J=~,='ӵI>D-8Y';VlOO)r1ww#ْK'52dˑM(-I;[v֩${^7yDw><^qm꿴ܣT[w(wۗ\ڔ :[΋<קmY x%;r2R QǷQYs5[<.eܻ]_ԁRʫ`KFEuשïB]vyS4\r7ě:)^E1AMm%9ä 훫RPjC76v׳U;nN9"~wEDr''Tq18zj8C!5;hm mJ< ͘Fu=EN/.*5,Q}/ ڽ_MTWͣIjf h=Q8 JTLɈPڥe#ܒk&J5$9s=2:NuAU _v6M`KUZRKRdžg5}Eqjwnu][<)gx}ŮAdi`s]:dS|M[d5Puڸ%dbikڽA[+N>w[Hfu%m+*Y?ödh*fP{W*Y^υ{/gS,EQ0-tsDa7UrcߑET[>/𞺅X8ttEϳ,vE,|3 6NU4Ŷ{odT*(Yb]eS!OnW5|e*U@VYNI Աۻۊ>yG{;dw|mz%tP?,zD@,`TP*v+UDA 6H#o DUzgϜ';Ϝ3Ԟ5uEqm>q.g%)/ g>{#hl[3%&`:6yg»֟^Wݩf *[o+& W/y3;.z0x~v뜅G9/=^^lsjN8FI'[ |?w#} uX_"pQʷlex=OLvF& FSQk.xKb`fLUF4pcZI(I-9z kC޸8rL@-r(&Cq|c3|kEl=IcdҾ؈Å߆f7l=*]C>|a +_xƘO!k?y݅]׋+{߳DOu/]Ϭg0h`^Y0 ocFtC˸)|wVa9Ql2{k\Xo=M\5ؾ{SGۆsGE{3Fh.̝u^Fʇ}Xh^Ķs֐ѮFa[*>)SՆ-6J-3d߿\d˘N8 jj>z| /wj~|n,3fԋWRCΚ< `=vËhg>Ş%dz 7V9Kge֠<0Z"6}={W}޾I4Ӄc[2`\=HkEYԉ-wNaMauj_ߠښ 7%gge+ @l_<!.߿~coi ,_UIJ, In6h; deffHf2f-{}[61l 6x/[){'6%ُ_ggӅmBjeyQ7gN>Ӟr9¼8L% Vڹ#fڻӝ:<ΚD1zҖ5)x"sy-"ɽy݃#-H̺/nKƗ1 l*bO#]0=}>vVَ'?ĊOѬ>ciKZ}<[~^fzF ,kbYs23.tl&cRLxvO럺4~8).Kߖq9Qˁx]὇1"|kxu2 ,fmJZo4],\3g 7(x6(̘_fi?_&ib늮um0#S7`|Jܠj4Ql60Ͷg ozrrRתopFY̝~e&#iT~mZ@m7S}x, fpڨn)xiCr*{29^>[۝k}rfu7 ͠ yY ` Xnovq] u 7ha|65k0^, LLf[oࠥ뗿/@7,|.l i? aòn/ +=φuv,I!"""""""""",";gܠ`pBp]12ٴ m]\?厽IL3Tn0/j[uEpm$7( v6P|6l_pg1up[ u=""""""""""({1/*_ pnsrlާ Vֆ z)a Om}/z?8`v`)h >_}m0'&f'ajXhtVBwгb; 0ED~}9߁*""""""""""""""""""""""""̓ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ """"""""""""""̈́ ""LQ)8 u4+*8Nŧo u &`zH(ӗW8}۾SxYW%-c_.$2;y IDAT󙈈y:Y3g"OUkATϓ95ۮ??u0=-M*=ǏLJ6.jO̚/ⅧY5?%!u?GYڋkF}|s{t63"_#4uED]SY7K6uҝ.զv61CY=*S}w(c9ڰz\M0,c{ &ޛ7qDf'lmى}#~H[$%lzSڲd\/^=/cĐNjsV0w4>@x۳8}k>F*F =/Gh/_ӞM|Y͵ocq {"n#.$GR O@H+p7JQE2i'l:Ύƞkmǰp|2^B/u:[nsz$(ek˩9i$/:ЮL+FMLV^8^C,lЙS"z4b]H|6\ ϻ􍞃{⧾ݨn+%UMN}c'P 0wϼT4(r.pvC?`D9offscxbx Y0Noo9:S"o跇~BအkZO~Rc+Ps/3f^IDwch< Q]rŜl}k;1ċcw} n~ᎁVf3x~I.Ǒ-g7z6W[ڃ#Lї33#) {q̮vڞnX OpY$=6.YugMѤ1awޘDv>Ǘ\w;OÁMOD5I!%ghZxdȋVDܬɴQf6= ebU!H,͢Svt}gnS2)n"ă=DblV%O Nox4b+,[R/̖ڋ;Gpx]'/GW Yݯ'㭽f99{ Rswӎãwwܗ5[.(}ORF?x&<:.N0fZ/7q&qP8gzykpG=Ct~h /ޙm9*u|ܐ|"n7厮0,c0 Vt F/zcaΙ7/z7љ~aNQFױzv_$ A=Or?Kecw90jbEJ)l{V*[$]2_H}6"*%'h8#$6>ЏRI̓jUeӟYl} p3^V.<ebF'j 'Vl!' {#[QSBҤ񤭩:l+MbT0-tGɭ S)4:tװx=a!~P8~aTY RF#}t~}o8}^v f^KѢIpE_Ez1$U+-ɿs{ڔC݁mE 0ň MC{%t{.êY1׳^m!x̘AJ#f[6]¼ax,j|;o#;H*bi1FtvO{R}L__`dzݏܑKB; aX|+2yE,/ ^(5]T^̤Nb甑\8c=c¬mM:q㘁[ W Ǒ86sW!W+޻&NdKY<ȱiĤěrmw5^ RN:Ț)D$SXK|~*Ň;HwɅmȽsi^l0lk^,Bg_BيWhkrU''7GbOF%{ v|8MudNK[1Dr&Md:F6;Oo>D #<s+{{B{Y'+)WChnhH(Ŗ Ea5tCBšj/ҝqy ێ@!ኢh4]o-| wg孴);/eݣ7P b1 wF^/}ưviD 9kE}$ѵ6V\"9/6Ժ͸+ yT6Vapj{ kFyT5z'!?X {H$v^0ߒ e|5+9kR>]ٟNlsiJoO]'氲 l`sܧN㸨E| 1#IN߅ [PX7NH߫zsb7<]P2o,;jDsi~V>T1i~m`g"ֳ|E8 {gŒ).Ȼ/- sS+X W/auL,Jpxɜ S_`Bfy:oۛCV\s)] "w=Gxy]6Im{ k ,.v2n gg_(7-^T#4wP;MH^9FSVm!!L(DafY߼Loޤ)\9}w[WuoW k~ˮKےbBiR`h}ɴ_6|J *ſ}v_ۚ:'_(t߼Yӯfo1oC1nJX&n9%H$i;CIvRrNշM|3-+ }uy=I^n6',#o#/ZmkׂKQ-zo},t[+Bm{"@|/w&{X|Tp5WECKys`Ud} 1ucRyP vLTr&>C'IXRL34qN 뜤2(z8Pwc|:[BmT"qGz;ՔEѸ=[[fg2l@IM$|W':#w)qEgQav8{hS;MquˈMq[ az׃չ=)ۤtSl b_؄QțL#m[Fc>6z #鶪Na㌛$5>މ9=MgrTwUq&(Q==G<-ϼ/&"-w|BTy(w"DPK㽉5'>G-g=s}%] \UT_}6*?2ޤSO㫩 ?B[GE HXO+?F|bN}ׇֿfOl> 3ZCB3>r CH m,o z=^`'!.x6$i@AV:"ړs`*L8b__(/::_U^%?{ b]Hyk{+Q|kU"x 0yeR}PGxG'),?',=Uv(F)}{iv~[9 KvN\ۖ:m#j8]r&Uf1dK\3?r/Q ( lfk*Ӷ׷km{P^|qa~n{Y(^EhUEs鴸Jfϱ/KMvZޠ[CȪzZ"{d'qst7^Y_J&lGSOe. lB?0louy% Η%ϟL ۍ?6]]N\X#u{Aj}8 =5xà4{J_{S\;Ȗ#1Zؽ,Y\)'r5saي)r%hB-)dB75jOd))*ǹ\qLI#|/Ҙ$ 8'&PTI +pIM<%0wl?_ '-CAly垃zURDil2I_4:%] Qˉ(:+Ʋ:6'e&Lm+x Zm{lXOЕTnؿc- 0juݬ `~5O/?vx1u YsXV}z82b6*w~Ts.œn*E?c2Jd.Qu޴sH3iPէ'G[Io܄/\BDcCV+,_bw!Ty`m : 5Uaսf߆ \ gktSBݡT^ni+5vl7ߟb aY;*1EEF9t\=j+*,%G !99D8aDMC 11۞ߧMFп~ٸسlڠCّF'̓NpTSMK8l~J:AO{ct=cG/vtYUx~`:J~>(D4̯j~2"7h8k۰ŜO/vsͷO{V #|$:x#rTF<>r\,(è*ôGv-'}xl[(tV`/0e#"nCQ&ᎎ4o>lx,Xȱh7= fZ<0"gyZlfu~ f7e rkaVrU8C%F٠q'ʖ/"h gGGMsS$""""""""ulfr9~%OZ]@j|9.8;\|y$/_un<9c>KpU{_ : NN3p?GsG kП_JfJ./}lY,\k [ z%:ptê*b/w቉jز"pQ X1"&a\\Jb&O%pʧtRtX9P6~x./n\^jIy[n>܂}ۍQwQ#vV6Ra҂?lEe\b 'R^\;d%n, >C8A'S]mO$?2]l;=Vj'͔ OOGHv"7&eB6ƚ+ ];Eށcy: IDATAńX|- d1!x<ɘ'[_%J OX1ӧ`}]YTϐQtF4{ _/`}=/n˘N8 jj>z| /w{U;|e\ych޾6u ~c5aFMfFGBA˽6S>#IJQ}]l9;+ oV|Bױeʹj f1ϿEߧbó 玜YK>$fl0wZYHOK|!֋v2?H 'H63/f`*f%'?Z"U@oɿyN!l0A'9}m+^B;uT%jf^Xa󘋨4MixGLY:`댧l&h7wdϝF{n=}$x |&?AL P-mue37lX%xlI`J=Uokx͡[RD m`U&?Hwoq$[_ѤQ'P{C,s^0pe7B%æ{Ct}g}{n6|E?_=r[A|Z-/d ZNln"GF*wKvl<"?a/ͭs֖KkʝLgK[6YUJPE{s~#<f[{Rd'Zl=5*j g#W\Oc3SP3Z7<_8Z3aώVWDDDDDDD4XDD$9{y&O}Zu_Ɵ} """"""""""""""^i4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*4*EdefleefU6qax?yxFVfo?i1M23.= m`<߶U@*(~aLYܕ46 'įLJů!+3!qk[}} c̠_|l@_Ơ- qdefp}\L@|P/~MPx2326: ~{/~yP|DRY\3((~?~~t`|tJ"Y?;*0>65 ΌrO OhLVf'GFOto ?2 OKŏHK%+3#O ?ӪY ?w ޒ :_nwۆ8⯷I#+36A2|V;̠/lkNVfv3 a;_< ǘ) _یڐA_䏛A%def0jsגf6B/ (wE㟶]A+Ο> 's׼]崖NPj]oe6zPjQx'xWz>(w]멠u?>#-0~?w'rxP:џm9mBPtN{((G]cgGrA9\??ڗ QsםAKIu?~[b`iAF9!0Go ]o|`ş ]CC늠54(>ҟ.]wGsyA>gnዟi$=]'Iu+(MciGF'r/3<0G=]݂r x9cPzo͟2rכxzPz۟҂r׻xjPzߟӒr׿9-!(wͯ]OrTTP̟"⟷娰Fr#(?nb>c>c>c>c>c>cc-FGgn#hQ\.},"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,"""""""""""""L,;GD﷎ tIlq;mN&Obaq*})%&q0k筴(~`\$N5~Oe4jӒS6o` ghܶ5'b|X`NtFGHHt/VLX\&yM ePZ$?d_xcWe!Wߟ*xFl"3ʳ]CnaBV[nha'J]qYl*5Xx?c\_?otSU:1VUť#wU1*圖iH4fDDDDDDDDDDN࿘ 9 )vP3\2ŹL`aj7 XĈXFƇZ^?9fPYY#؇?RQ]IhX(iv xZ#Z&ra[Pŷ̪ f7iC9,fK]m\Zà^3-UT&} ]XfOraqDaan/XG&1Y4&U͌݅|oA3qaWcD &0@{n0I q\W g1֠ ^ { ض,vzHv֏%n'U. 52{B c~8!= +Ȉ%yWXsD1>59Wk5Mlľ?3!+`sD3==,lEɤP^۱g6jђ^%v5^m[p??{6{$Un;nޫ/VZe+-X.PxΨ-URksYt2492 Qq gQ^%q Y[B.%-SNG19=SS9%tkʜA3"ZسBby>a :%p9T!o̍LWU<^{4[Eѣd˚U-3~KLI=v'w$GrxE?E%Mu^[} ky UrQ}yܼU_6L'LgΈKݥFpw׶r;psaIJ dgN>=$ydUaϹn-W3F4b'h(( F\GMx,1Lo{w9?7f~Uu{|sj<ͮZF*>]qyP;39Q)i?vsojẞ~1;uVx(w5EX3Q ku[[ȷGGb[a؁ߺ)GxCUK<>N4~p%\'r?ȫZcv:⶝;f8׬iT8o*;R|T LdwX`Ñ]\Y5;ƹg9Pu[qӜ93^}T;oJ9o,l4*֐u#K{[/_L"%,~1m7:k*Ҍ ڗ%FddMq "(Zy,9[yWೕΙ8_B"1ž̕0)/AC~~^lFvo!DA˧@s8Eg Y>p`*`_ [9Z쩵b !ln/5P _+n1~^2` S*/0j)8O4A:=h5Dk;S,HǑf ky8ݫ%N3fJ%ڰ-F7*oxRܓNM㐸_ttXɱ 8> w݃ji߸7d^ y?8GL-4j5(yP]kq =h DR`q2X^X|7A*t DSsCCn `K) Y!ε0e\a [7\<"\P"7ϟE.՟N̪S@`\-K 4V5=ə ]^4쭲mtFV[ FL~0df큟C;xxCƜ#VzBgG㙙8,ޖ/Ʌݮ" /Ms]9ɉˀ_=9lC[BO6\wOsɏ sf}oTu5)o!?>ΥxS]֐ ף}0 kZ=vKpl:ų97MUAQ^n ؎ nur~C}dh,Z]|>B+Ӓ ?IʼnMpP}tӮߔCH8ǧr.9:Z॒bj# y[JRXո.ؒ$gY*K{'z=8JbiNjZ9#>dA_g[dȝ"ųT-ao)ޙN2\M$nQ*.&lT&Gxv?O吷5ˇ;Bɷ7 ke=RjR*BIF|u 1GwG4[;9=_X}sf}-~@Kv0&jm=21^wYb`OJ=/9c<+CX9Y$LNݙl&""""""""""64M  SarFo{'ǹ+ܝㄡn^ZqCU<|n,00;=YJC4ǥK C&?;21[ڨ muBVnrQ{q9$NQ |t|N酝| Y>~*ր?e3>p,rm1f=:mQsûb>}d<|2>m-I3YP.ۚb"yc7ؗhza(<0Ϸ'vݪ`8dG"U9<[S5S973Υubwug> g$7TXxwTVܪOÉݽ4bqzL"ʥY>U&X|}!-|/L_~iwsUkH> w{׏)==3$bqMO%# 3guCt$Zz:zzO>gb:um=|dkʃ`u7ηr`/OY綞VV󻷯\~:g,W7Fպ9glmíT#HNզ],'< L 9(O<ǕQ?\^24G?%}HcZJl'(.Dz_""""""""""`UُX`D[I! q (,oe7Uoϧ& 7|b. \u/muz)i`hsE~Lr$BGphw,w* <2:IDDDDDDDDDDDD xx%~\LTZ2IflX{ñ>8\rqsvT z&~PЀ K\5Q`>\ _#""""""""""""~OOI Y<\=QV+""""""""""""rR|Osv^ """"""""""""rs""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""4)f9ϗ=ݪOӆ8'`;Is|? wr͖!n>9{Zc#CmIDDDDDDDDDDDc p!!n:P~|XZ8x[;eyϔ"c^>//ǭQ+wqlgsa4?Fy{pË U^-p1~+&?ذ?m+_=S-=|)Sa3.Ɩj4wZtv,w{:ya:Y>OdxKO+cy`f+vT~7{|aMҸCPE&Y_Z9~QX$ye;ʔ\kz\ bg*xzpXNYHsS|jvusfڡj!fb_GkZ-auK&[9q=ʟt'ύ9Κ(q]]v0$" gq)r_L'wz$1d7U,tuZn8OU4? wНm2TiroU6Qge,$Mȿ&*7Ż:$,L'V$Z@ǸFd+|qt.gڡV殆o4hI~$""""""""""".Sʴgn.I+G727Λ!&-[&YYsKtk{Zs<9k!m|/qj<Uwf8c@kSܒ+0f?g!F|?7+^ܙ;EJy^ǯ&r4/8D޿ㆶg(Rr䗹3iᮆs3 ۼa\)>@Xk2ߛo B&ZK:ᐨ}J Esx^X@Zm ̗+b`< upPU CU%`OvMh*VlI,(d*RdI:X('Z7S,HǑfA;v %VφzV")繡<=`ʣKrp8īqRrs{ֶ6(phv<> aƒv"R/d&</g8Ըk9,2X^X|7A*tw> E(F'3<>#""""""""""">̬zm\i n trN5S^XMp0͖Z7ОD7.0j㜺 C[,8/lOr2=kϮ~^)N`|)…0vӜ;YiIFC~U/br=eV.`$vpb"h:P#֐q*9 D F2Y5n*􃀹XMEXxk @g:p,84%(VJ`fBbKs+D4[;9=bl{+qNHՎ߸.ؒ$JbiNj5Jyq?ʺ ƒޒS-^?C xaq9!SVm~+v%tav*P,`,hz-ʓ)"""""""""""o_sCۂ*>Svub[|b 抳|ds˵ցLX0岎n7s&\X,W T\::/2k+|qbJ- 9 \5~c>3 ̧ @4T!T6M˵X,E>xO$ߙcEnWZqr>^5+=C"gdT2b0{KI>Н8 . 77ƧrJgč3{湭y|míT#HN1tCZBsg5Nre-գ.frlΖ?S;NShVbnW]QV?]1}a{W+Y:EDDDDDDDDDD䀦XdZ2T5 !r,A榟ah>]W"x5!eY W9>kZ,9^ WfB+YCh \-99h 胞 I6} [[ mnR\xoNGU$m%1N1 v$q q c0xf4@rc'xKCjr\]0}c,=Zuf:ҮNC-]ju8J<ƭg9b/[x>:R:7yOt{8wxWW=Ǩaյ%"""""""""rIz9) X~rO},Aǂ,o> 8nT$b!XEhŨQ<0U }MsF[WFČSGCYV.qQyjLLVc2NMSWN]ՍS17"|,##vzmވaʃ6iZ{yrDmooٵK{xin;T4NZad;7W\Mq%QbKǫw:.[''xtp-C.Tvoa7?v„b)>2WG-őq7ov-\8/,,7I |=9M B kGs!5,qb^Rs;,WtT7_s\_ߕ -.5~y05=.13n b<=ey,wvzvZOr)MCzBԨu1\YZ6&rSRZ}޲uuX}f1ՂiMS%|[6 c1)޹Xr| 1܈&b)šb.Mfyhqn+i:=҄d"K<-= x偙)M^km9ař(H$Q欝exWă$G\, ZhoNxbK9}K aFrw. rmoq:bqOZZsږ1>p- ts;?Nʵz~;5kGMr\Wg]" ''Qd} ,XIsǷp&i{˚h2| s!áÝ|~\n kgkŀ'4y>=t_>^XZҙg)i޷%Jc|e82;;"K0.<#q=ʟt'ύ9Κ(PJu.GpdfOGt?-(.^0EU )>E~ȓm>c ge]72-!dYiɀ;f(P(KjÙqGӐ8-1Λ l_/ޕsHrYѮq<;88Ag WْOwznJf;-kKuOvޖ, L`8:K&}wg\gQI=k%r⸥I._+k5Wrնf}Zҝ]] ![U(%n)w˗-e -lwdI-<+4}`>Clq^Sb!ڸrc}8mhuPcyv(7@?e(qBZ_W8+rV~@qHIj)l_%hCLb)E!S!"K16f'c/] J]kkK1uiObld]J[j~Qi욵bXMkeۥq[S;.S]vizu>sSʨzKS=7nlשk-UqʑegDfl[ s0_0e{:_j!Mc.Gg{yb(3P@X{`[,ƪm*o<( aHBR@DxZ)HVw73fX%TšVޮתe[5?c0 ߯NQG<uth>0|qeEG/MAwC"eߥdm>9͂ oWy nZeqӼ=⫫M-聵ͅ.!'8o7}W|:[4qMh֎3\⃆㱵cΊ5Ԯ6yv8Jjo$ڭ2%}n(d]IpJK 0NS[8 ?lgha{T!a 8ڃno!iIINN|%n-)[8:y$ڽku,Bn[SYޚq1k?J.[;9=n.4q8v㖹T+w#ݫ9מI2S,1ٰ5/Lr1޶ӧ?^hiuϲRq]HDDDDDDDDD{|r8ӒX,N^D4ۧ 8#ա|X9ikI.7$?YNtsY>~*ր?e3>:khwv`s^cxեѩwKJ hprGjk6MXm(1\.8vyh[_ש[yq]Ryv9pm8:-mPős4ĵˣ#cY sc06G<]\s@׮ݰx,a=l^7db,usy&,'pCGݚܜȾ^mvfam>n%8mXte郝1'ڽ~<0mdbK+e _3+3Y1޵٣8?dyA%ͥY>4T!Tnk͚8֖ara7׫q*ߤ| M唞^8&˧3 ՏŵKwLܘs gZ"k&e !C/CzB7tN~n[m{/aXsûb>}Ø\DDDDDDDDD2jw]Ne8Zl.W_n(q*`s5Zn}gZZn꯱6e慆G.c[4#^Z>46֩)^+X e|d\rXxxGu >8sJKv7 n&Kzy8ڤx6~72}"""""""""r}<#h@qq\c,:8r-a9`^Y^φѹhs`[5Baf!lmoO6+CZ}ҡV^/ḗBm8w@Ӕ+}qi_kyʣc=i4+n~RmK2P*j / IDAT ?YG734ŋa墕!6"yŀYLX?^Za_,oybGܺ # #lÕ#j5|^0TFAt7my4}w=5FVDDDDDDDDDDDFfH """""""""""" GM """""""""""""r`P,"""""""""""""rP,"""""""""""""rP,"""""""""""""rP,"""""""""""""rP,tT27isI݃|3t1f[`+++[p''>ѕ#NG:g8Z'g&%$"""""""""""DY޲}ϕWyxy8\ظiޜu1J;l9?l;f>vH:xk3U*-p+nVOOsg:iIHDDDDDDDDDDD.&դZzR g]-sՎiЛN$ >ߞ⦊͙ixT⚉9~a])ΜC2=?I0ݼ$pp<;vNp]i})ݼ%E>11]!RY 1lf-#xWă$G\, Z'2Cϱ<03ŕd&B2qC=;''|)š8Dh3J˵jk)Y>,osv>ܛ 3]y>4Ht=$#x7~:t܋KW,p'xz8ځcof/=rrG?̎.2 /K AyLG|wN4!?q}lŽy? ,sSc/gvc禹.7H lEDDDDDDDDDDDꪾO)γrͧZn ge]72-!dYii5ۦ3-3Pasg^p1n Ne/sg<]-ؑMdSvVDDDDDDDDDDDDZKŖr &<p,F*w\8+B[&(}֒N8$jm٧5N*`(OK8$]z[d塘*rRuR@K"Nh{!\qwCFnr)34luXblPҧ7FO2bDu:p!(ۀ[KK/K% |Bǣǀ Ɖܭl0N>("""""""""""rPS,ka*?χv.T(?Nq`;HafkkڸB2DkY*+vrmMpS!ڌpuٻkؚ<.V 0> YIx6Ω[zljp'k8jy4JbiNj[kӜږX)i. 9ʋaRdi98q<)ZCz]cGIl?c(0Zqc0GFE>xO$ߙ,PS]\e U.W%r~'_xP ~-E Q8w )./L?k00cIR, &TWs|}ݽ챺Zg$νukꮥeA%AM4 I`6%_T,nAA Q $PJ QC4Ȣ$Ggz.GUu-]ݳ1<׫=sϹݯz9'(C{{0Ms3yAKී#ܼ}/m'_.}(bG"o[!B!B!$Na^9ɏ[2_4NnNi^ıq/ȹ9 G:Fψ9&3Üqt2ւf=w-Sw=kPV g4U-sփL`T3V_7L3V^{Z[[kjJ5?c޺_ڲ=Z?|ֱOɴG-AYK_b7~5"sҲmӻ]玱zH=mO+qT(Ou\/~RSi~'Vm*(& Mנ"_Esbʩ27LgW8/WҜ q=]uxg?W :|`'B!B!%CpoVD9g+yBr#qΎ`swT6 JhM{眷>Xb"W SUU5x^bYLo196>G+L;ϧҫjbZGOKnF9+պgk=駻Ѻ=8n)B3ُςH<͎3+#>FL TϛRIgry޹kG0|bk¸|o>͍ޡ(ہW坶x^嬢XJaQ2J< ,YYt } .[~K|,NLBD i>u ?N2(WlRϱj!,oK(Wj.covj571dgrL")a-r|6*%]1?: sрڔv-iK"\=|%blܿ^ԛ˒WxsŰj2K |߈A=Ý0o/g̭.rb9G$Scyτճ6[!))7)a\= a(K* WpNOS<33K|wQ18:􈉩 |NkYyQm9:3狃.NH%Gxgd_z(9d?pL{'K,W^3{g9c4Ü0,06+\Zp-ӋʟB!B!M0sT2#;i9q!NrPV7r(P(v:%&1i~ܺAYUf9k%>0 ps#>;)>/sZ^cߛ^Ò2\| mZwÊrZvxԂf=?wȜmYYMU *@ZT[p~kAWڲo}mֵ׶׏cf+wIJ;Z\~W4gMs6[nY-x}XZ6p^JZҫ7v5gZrr~|L357č>3ʌr,c|nìE޽DZ7o64@%3{JʌXJFAxSw ,C Dl^?!2'FxsEM6yqaxr~6$/.rfV>^36)(cam?W]rcKUܞsqӳY~?4_|`*<#\/W[,oޕ'F˭2Yvv_Qfi+2htc!n)p7jGrsf;[˲gKĩ5/%VRAW%JA$&I?Z.'sli\ ,MV~N|MӼVo6= )=6<h ǒ͎ Vն)E4nSEޑI?_Z'kM`n{U9*/9!^g`ѷ58, S*,2hf85hxϺ `Ř7s gnKpb5gG⼠+ؾ>>>*%*CaYB!B!xZ@x8wT|`6^e<`,5kI)wGrڏ"j9%Pc A]GZfѪP6 B@,Z+]+g*~: hJ˒ %O4Mnr\|}zf&o#S蘦|>q-2w)"F4kÚ 5z-DD SAw _UF}Vkyʍ^Ղjnl vfNFkҜOi 7AvBkT5aΆYt\C5Ru >/W,&R{R9:hrdrߨiDk)pk s]JJ*h%% &FqdEkvs}YLI>7`21~jvU]F-cP]]lyj"=*Uک.璨Ic ԯUQ%Iy=w!4J?>m|O{*k'[v\{d9x@v_YQ?ǗgF6!4_T;_dy gTgp B⚹r]]Ŷ}\שֆ3GPMj/nĿH"ZSe3PaW}*G!VuC!B!B  zL;ϥSYvnxb2.Wڷi {C7{04<ݮRﺚ:4n'Oܺn9qrGo]{-n+G+A.uh91S2tc=ډkm Bwwmji5v7hWk?۬3zdt[g繣KtDa6.k߻5{uuAb5?XrF{>mXݠP<]x@g8ה=ֵ~w\bgr\$ڴzRD\7n:*^-L9{$i6 .T{E>BdE{}a_~G6g[`nzeuY_YٛtU}/ٙY% lصӵw5VD[#>\M2 6B!B!B<bO29>P:6rF,@h ZVm95S9Lպ~˕ 36ūAs)}٦unus]m zzykFЮ\յ`xmڔύzY^,on9ݧOF7jǩqcќګ۾8z(mCG}z-׎֍kuӃ]M)? nဟm3@}ڴFQs+|wNT(qdvV6p0 L^soj6(KW(Sqgkojs.ܐ^PELT}{0i}"䰹 5Wb>ɖkY[0rrߧXIsWiKWyDVI[##,䖹-u5m<*褹eK79~h7~ŘJjaV^ױػD{oIx`QmsD~6__ඪGވpPlܿwF\-r{- {. 3 &LSJW9rp[>)rlmck?[Ճ\Y=vyJ};Aa8Ioŭmv:~NpVxs*{6;=0\<aH)}>ʩ/6w/r N$es4_(٨<A>?钮Ul IDAT8e8C s,WPY[mS@7m[.޺Zwumum^#ud ftަfG@537l?.V5Ӯ !B!B!bHOaNc_k֪Ymږ@-Zh|{psE[pq@khLgeF`+]|sQ `8G] LF}u[=5;حB!B!B!$,rZ7O)Z#B!B!Bq2 B!B!B!AB!B!B!B4!`!B!B!B!xB!B!B!B3=v.r>& bop W?C1 1f).-a/$CD2?4B9- +7ϛ+[!Ί9|w!?>d`s*Rգ\rB>X8ɕqYe;B!B!BC~eX\5CwexWn G ` _l |au#:;޻{9˭X݋3];8J\5Ub<>}=]P?YqZW"'-V嶁[Y\[ 4L#qpL6?+e%\yB!B!Bb$, HON (] k9; "͝ KQ(9d?pL{=ؑHq!KbQ%XDd{K20a/<6J싇\ T.L/ (MvqN,/S>Nͼ]a7KZ}=@KAa5dq,_5a_mj[AN T5X^ ?N2(Wli㺛.LJxvKrz/k8qv"D?UXՊV~|$+-M,Y8}zb` 0x+d8'SaW,7GǓ11)pM:/[ rx[ yI>Ҡ:w0;eޒrt\8-qUńiL_f7v|(+,iWbMVF <"W8G rAGvE^,KϞ(GV)p{t?Y(fd !B!B!8HXtUmJh)ͩlIoLq p뇢?L5ŋn/=u,3,Wd407BRڨlE;=Ƿ+a1ksEJhfl<<3[+?4;Eڝe (F ISEʘVK@E>(MEX䗃ATy~R QkvexX+"}<ç GĒșgb)JsvyVz}_3pj;|x =˦I9s%UwmK'"7T.|O?in(}UΟKxN|Y>~Xpڨޓ!B!B!B!gH=V@- D9# m;+y~(QuyV}a4>|0$>S/ȱVu_8gFA1 *i ?ċ,~7s\ خG%F25Mq|[MfئxOSq>6j3x%n_r0/̭á(/ssr1vG[]]3hp*+||uE"צ? [8':in);WC ;Wq(ɕQ(J,; qxcߠohk]2|YZ1 sBeri?玦8AC ̣Y9Tjf]NOpLhr7~ŘJjaV ޾unoge$NpPNkXqV+|=9{tgW]]Y6%:ӓ`WhIer7[.:S9[wBk\OxPŠQ1"`Zs}抜ia9Vyj!B!B!Hߐ[Ĩ}B]1N(pAnijG$R|N{M08Ս qζ~Op?냕,Oω;:v Opr OPA t.JrKi92:~0x(o.pu-B!B!aO DW@DVv[MruX;Gs&A'y]`n I\6]\UT}7V'O\8fSbX&@k[g?MD(Wbq` V]B!B!BCd !B!B!B!=3 DB!B!B!B `!B!B!B!xB!B!B!Bi!z7>&`(P JշQjhڏgj?F3h4嵪צYn|׭ֺyRFQ;VQ]έSzvҭ]o}\]kEϓ-B!B!B=>)L 7/S)r$~8L ]A1#m*GxķXl/PX J@PGP) k?7j[ trq[P Pk:w_H EHA9!X{z=-l֣4گsZޏ`kfWqhNr} u۸o?ֱzG`I{=m̉7ƗObjlߛdžnr1ˍp$ߙglOF&ǹ1nsB!B!d?ݩ1K=\==s \>ƒ@Xfi)ݫ861"9#AȻ1ψ9&3Üqt2ւf=w-Sw=kPV g4U-sփL`T3V_7L3V^{Z[[kjJ5?c޺_ڲ=Z?|ֱOɍmEh o |k~E2̣9Zmӻ]9J!i+=)US%\n>QqH8|yV9/t`D74ca?A˕4gC\=:}k~4gch?G9i-!B!B!MsaqH}B-J,g8x 6w.,qGe=W{y%/i}U0Umy!"d2]^ƒDL0^R5kVxN|"s!e*|6;InNaZ+-`ATf ZdV낡>.[VmS*7S͠nz15=4Vkh >}Zv3:Y=[M?ݭ~zcAݯ1*9ʚ,h.sy!092c;M%)|&睻Vxq3'&,Q6 XX_,/qUi{HU*z QNp|? ^,3Hqģ 򙘕.J0Зx..7$d lOD١`:CY/3Abq˖J,oJrᯖ\2V6nV#}\L{&$6P"gypk"\c09 M|j2lា*%#g]" |~NIpb'#W,nj]CׂJeBھkmlVz^ ,wZif#Ϛ6lnWݲU[ p3@jɎ֠n;k 4ص2ǽ қ87 Og8kʥo}|gj]喙YnHG}=Y#[{nrlhJf<# [nq);s]y["6U Ãs\#9T䊢&μIc0cQ.-^k|J#VFh;/ViO M4:ͱ Y c3]_[!Pq]hci|'sli\ ,MV~N|'U9*/9!^g`!izܝ@0>|\75Ǐx}+-}i075\ 9)Wφc dxWE"_BB!B! C q^U~ܩflٽ(x/Xj CR0jovl(򱅎 򣢋e Ă<ҵV2/㰀,P4AC۴&%Xקnf62ei׾k4UOuRcaZFeׂAs*nضj#@ܨj-OYeւjnl vfNFkҜOi 7fpƺQ-*qs6=͢x+W|´e1^ڃA#FL#ZO[XWxGU7xLRPVPA3_,X=(H^71$+Z3ぶhCfOk XL j]UQǘ/;wW([;HJlv $j9~RX(h ȇ'"kn`U-BToR^Oq9ǝz? 渮O#6O7'pveǥZGoN\97r\wH<-nZ8aʕZFvYtی/m"+eU>4Է-9%9m }V+zo9vK܏d]mt#.>T nT`h7y.ʻ]:u5Qmuh+usmݖOj\˶fuo8zڻUm`u[y=^wC6ᵜNTXnlkFB5v7hWk?۬3zd.Un.i /wYx-75{uuAb5?XrF{>mXݠP<]x@g8ה=ֵ~w\bgr\>n*ۙa^33ǿ&2Wdx VUSGqyÊ rbkg B!B!8dbO29>Pf9rF,@h ZVm95S9Lպ~˕ 6ūAs)}٦unus]m zzykFЮ\:Zs}kS>7eyϺt> ~1[Es j~n6wurh_ \;n[[7ե%ޜ5ޔ`VIv=[p/*TM{m9i`l~Xmt΅bKJ򾈉}/7ڹOD6T^?[*?p^=2-@(c^WqXw @Sp1H 0aN뼭 IDAT5V9o6G߃.WB!Q1rxZ[O O,MQ$Z~b[ 7ZW<-&՚ _[(~ΐރ. &9obV`rŲZ]D4+(GzD691M\V:Tٰu=K1%,j-ݙ=|]KUC7kQY֍T 5Jci-ަצ1epӺwI~o'v7ƛp+&ty`$?arN(>1c= s+ሑazh{y)ݲ6͞%VjҮI|+\TԠ<wbo0ژi*n+K8(eR˝ǪgL[lUOsnVNWi>ߧXIsWiKWyDVI[##,䖹-u5m<*褹eK79~h7~ŘJjaV^ױػD{oIBsS܌4׫1Q3`ضxnO̬b%F83bɈOs(2g,q[ltc bSIm 5ZPg{kzF_g/Ua>#v.'rG"Ņ,qj+Cv_c/Rp=Zr-CQ!B!B~r+U!6hWϪmW6C]ևպ={UNgd0ٯE9mn2AZI= mS,7ղyYS]ӣN^kи.^F63hkA6Y^gu] Nkb/~Ab|r(Wpaqf HjG𠷅ͥAotqkB!B!O bqZe4ͪ%a] R7~^ \v+ue)AVU϶\yno];6Ѻ6:[3QQoSMkmNОhB!B!B!!Ȑ&B!B!B!B !B!B!B!ӄB!B!B!iBiqn=l&G8)}|a,ʄ:HmC-B!B!BqHpد k>0[tj8"ϑENZ`裙aޕ4}:ǔ>8g9>檡>s6ϢtE!B!B!BCEW!))7)a\=~ Ñ8gQDsa;*T?Gnp;).dS3U\L^=>a{l0uX/,ŒG8 rx[ yI>Ҡ:w0;+DӦsInk*&Lg|~6ÿχRjq.rVZyo" ?u,NLBD i>uP2[_- Ñ5÷jKU-yP>,B!B!Bq(j۔"RSwT+3»&ߘ"$Ejj_={X(gXhanAٺuSshhi޷୕׻Zk[K]+.y2Eeʑ8/ DzL~+lJIWMxS8o_YÔxԃµd\f`h5??VsĠt N B!B!B!8dHXt|c% KzUXrR5 Bp@vc3GPsC&T(h͝ɨ2t˂քEٙ۲8rx\pN<2otxaPDž"wݎ fv|a!B!B!BqȐI.]wP#R)j  m#e9)WF9-TvsMR;Ƚ2*kn(ΨWzEDF\qAED;xEdAQD7kͪZrX*n.yx8qf?؎C:g;I:Dvvga̧쿵 rX.@斅y.dkb a_'*GĄu..80%;~'ߪqK2̆7JPV}0'<&j? (p@;`*`g0=[Bϯ7؛p{Zsn\Q0C.6?\Ej.w.j|oKz|6=6b3ݼw$C; 4Oi-$B~u5FQc|9I>4掱"wYv7lK,>e?`3}\npDu°6?.2A/|7Fw#-^ϛCn+p ڳ#)NZ|m\_'2Y`[S$"""""""""""""6,"""""""""""""F,"""""""""""""F,"""""""""""""F,"""""""""""""F,"""""""""""""Fg_~HϮ{Ik7&q}rC`0ElQܽxUdύB$ }Gy+!Yi\7u>KDk7_֮a\a?bhyDDDDDDDDDDD(JTCaPcsi w'xmYa[:R B*!j.ʴ`/NCkN4+Kם^u[UL̻|V:h =<ׁNFH!8z8āelqLin|fYJ3fXd.=!wh^,t_ccfZx<sp\>ɽc|Ԡ7LMx-)(Y-_lw*LMrv43S+rtsEB*˻1XtZ;C5 XaH%|H˲;U)5M>,ĤÀvDo !g|nb_Fzr'G`A^ј}o券G\nⲭBWL7B/ >3^斥bޑ/ږE6K 8z7XS)q\v\y{:g8gc9~N|$r\8y4y?_gz `4cΞ2B$T ^q-QדE^N,8m:t[M.R)YH/{=v$)= VB€isdTsk;ݎh 18S:Ǘ=BQ ǶYqL6O2M Н`oPb}]o7TzhώZ=Rfz9?ݚ]Tp{ 񾚼L._Zٹ^Kڴ DNp1>O&` fƹas O./}Jq$?4OӲvu>?5XN Gg5I~6^Ȱ]8 ʖC M6>=9aI݊,/7Z+`&- i[s27C4{G,h5>2"$;-;dipX߅tg Rh|~E_7$'7B,'rQ@c""""""""""p*?X18<̫ ~{Ll9nFb 6 Z0ޱrWcF.xTA_Vœџ͏՝$[ˤ(TgmOK{! l;5ע`%V|KypqzY;JP`yl3]ܳemfk- Ϲ_ 08<-9%ό b6_k.nKU9\)sFkq~Z%C|+Z6=ş95tuqBoPjTeq_+8>W _o~MÉ^NisN-|މ w^~VkpsM66wϷ?᷸my2;؏c ImXB禅6_r90_S6e D,DDDDDDDDDDaT~"XЫ{Ugnh+x >_gӊ?fhA.64|w[NR2/;b]? Yc:tc2O<16a͕EaH-lbyV$,@>1oia7}Ru-+'=Y6?{FWO>,Kwgc-h2e,sno7N{Xxhg-.W /A EkmGc(dczC"b-.b8o⧁ŁCB;*6WOLqv y$V?F(XH2# mV }qc%~x'qt,Ej{E|Wm@h$.̀&c$(%oxy(]Y#% z r%Xd`H;Xe[8Q A_q^tD6/Tr}citkhc '؄TBnq̺]S 6ܻCMMpk×pb&f rlBjzp1-!Kd7Ðtlns8ŧ(.F-9*aRcqb,v_P[&8c/}'<൹ vOy]&,i=&kAFRo5a'EsK q>T ТY=S< 吤]xL;Z./JGpˎsLoޏDDDDDDDDDDdǴ yDY9.4sl6~[z1( r1tn }nyep.*,#f<=5Í@-pX4B:OVԛZjP$ak]~7J3ٝ^\4- ʕo۟sfc;l‡G4Nv2@>d[*SiW#LO4l֨ z:2?T6mZ= VB?COJ<,s0,JSFh Ml{6;h@ GEΫ4b{8$4XEymh{s2jXFt7ǧ7ׯ(0hUΛ1\^(yjBm<+ czgzjWsZiu?}D Dw'57K9n16^|W8'1l?m5\9{e'5^qTUzs}> 16yĨ,""""""""""˰zޡ?#GQE9ykb&9{c4OTؼtpsvue;1뻸{lkzg#=O8kjuõn-kLri5ӕQĊ ʕ5TX<5"""""""""""vvȣB`5B`5B`5B`5B`5B`?"gC!T9oioji IDATxC{l y,Ugm۾r \3Vv05 SUN6o7B.j_goK:XFfl崴E,9nտLX绵,e\3n<ša5 d$ ĭV]$ʜc'7Ghۢ^žǷ&&a[7.!Ķ wK|tlehve!ppeB*˻1XtZp`:!_EhZ|uoh- !x_5|n.qNd1\;*D9rzĺx!œ]+ϗszUΝp2\O0h \1 gOUXz>rn.ƺ͖i>2~NL: >_k.gz9.i6 \:5ow2ˏ1h Dϲk1x-܁X3 غlC75٨|jX+y/sKBG mˢR&xK{w[ך{75jcnSrW<Χ6fXE )N)⾹[]}b,oħ+҇lDAg}Fu_3yS3"Cwx찶$9egIǷarZH5C5 XxH *hL_؈ȣI`١HuKEmShpࢱ - 0U\ۋ&8,ph&FxE_7$'7B,'rQen_,{nJ2U~,rB_Ɩײ 3YXӓN-{xV3ےױAøp`ߏOֶr}F}ӳ->4>T4X\GzMp|5vqHC(Db 4ê*>UhPkVyrRw$? .0ޮpLky:oZG8e!+%Ώx_Թ^z0Vvsp/1^rwn~Ȝ$y}e~o,RiX6kkB ;˛3{K(F# XDaί(#ak W˜X8t`/٣nSEN,^00љn xr6A',xs}[>(6OK4uƣQ&3a@&ǎRd`]Vas7GKl'!^✺>:Uw'y{il&1#)]z.،M骇vqp+e~c,&_/K;d[^ vsEO5< R$ rZOJax>l~xjO3uIs`Y1+[!wZ| '9_H*ch&],`:t9v۶:|Vz|f M?ˁqEX,ߢ߬r)P: (EK] 1Qa3 V5;(aSA,67חvz n3Zi:D[%.!wU?|oW1148k0h *w# Zs}=i!z51ػGGȝ5grIl=mnP/?5V&o'qm-Kkw->w8 ǨdȊ| D)xMFչZ>>mo7X{! Z qx4PoztxST|5amC~Sk.2#1ڍ*[oSrI ] LL`~n-lh4xK2(fei ۥ~@']F6>ln4+/YX0m~4 OӶI-(1t;^[6]?ͷi-~Afvjugھ|ġTTe `!9.>|? ,U>x2 W~6WOLq#xPLWkٿQ`%8n MurO4B^~nY@)>[ZzQ-ؿ΋#how*γiD+MO.L&xAolpbZV3]WT4m96ΏbqN&8və ίغڲ:Ǡ );.ŧ/1>ˏ/Eoc3ݲ,&m׼X hDy^eSyl*ܶzwGKY><w4T\{`o!gkk695?۴4ZcH&xn&ϗ5; CQ x0ߛJw,T=# !x!I *dÖFope+Q L;Z./JGpˎsLo+hsRcS"Xh,ijl-{!S1b@78t[)1oN,N6k<'Xp JG"vC7V?F(8 XSzblYc$WڠrX.@斅y.|ۍ1Ӿ? I{X;.z e>9o,ҹh kkwljۼ+ʟ nnEtm~UgOMׁ BVVv+Q{v2NvvHD㲟qNv7[q6,g&]ƚ]9j MZy̬&md<pM [>$PH&wk~yjwϳ}~_px y`_e{ o]j Âi<"^5a\6m~2[ㅹGՋ8nv$h5.\/`C/`- 5ǧXf_ʄ<[Z-`MPfū\: l xsge[2sWsI>֛۲qU7'K][^p0xK]zޡhT5j,e  3G"81<ȓc|a4OPvOE6-Eý%~q\+)* h i<*TY#TY#TY#TY#TY#T^,)pa߻a=0IkcY9 \~d9ZW/p|4[{[vsUA.۰t1:2.~cn]ng۳a_}N$r\=4v6rٓT|p9m}Ov/no;΢޹"'y&% |c鿯tÇsnw=eLH% qY>׌mՇ=¡=ILet̮zm^-ukio S0U7ht̵ 4z?kD`BDDDDDDDDDD'"LJvyn.V6fRTB68 q+{iչb17ȇ)q\MëP CMqZ_'1U}B7Úr4c Dϲk1x-9>ضrU},7^;Y^Z4UC\[਱ ӻ4x[! \<5ϝ!ez `4cΞ2ؐj{!}Y@qĵ;]}b,oħ+>PHeyw6Fߞ.q]t{'F].nKE`' ):X~Ыrd;ylDAg1>h_k=Ql帤M@4lp<]MqP}1ˬU7#Zm{g2o.-_^>ټzxub-s3|tMUYv# 9^5-Jmqxp?Oo69(R̹ns8 .Be{'\4v$)= Ku~Fgܲ4`*֘Xv}l崴E,9n3K}!z;T~0mW +ƗMZVe-XwB_Ɩײ 3YʰxE_7$'7B,'rQ]N϶8o[ }&,o-%sQ!67+Ņ#YO0㓼}0߬scuݼ3='7o.8?~SvP3>VlDr1~35I8,Wؿ|ec,gIqRc4T2ϗ26Lw:_-sbЁudǃv(c ##X6vm;=:CgVZ767X_f1148k0hK}6Xϧi$vA]Y>3zUklɮf8Gc_gMUac{;V/glo7Ҽ{! z{:؍N8`:t9v۶0(m~Xf1qa*?~'-L2`%8n MurO4B^yմzbRt`B )f=^-cwĞ:du4d 'ZeVF l.*γ!l}##`ˬ4ޫ|dsIe-\r+i=gv hDy^eSyl*湕{7Q,093^?չJqx瞶yTɎ&9˥jplOtk< gTkRK/?\eD(`9.9t,(d$wnb+i<7.AC~_UYVbvcqHrH!$c<<=rOk$&'~i47E>@U#l xsge[2sWsI>֛۲qU7'KZ,<>s}> 16y\5>uj{ \c_KܴEw6cwo~5 .R.r mc5P\*cTQ(t@%le~ީgtޑ \+<} <8Q!L̖%C1( r1wvrء,ϵ,]_1qEՕ#gƅ l( eTsLaVsEAMx/53-P2l<6./]0ڀbBp.*,#f<=5?űMyu㨘2dB7ȗ>uC IDAT?[<ìwy,w0UZDA\S F5BkED1&ັQ *h}GQ&~nI$@JzvQPyvĊ 9EwQT^O~"[(!# Ο~ޯWfvgfgw3$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$%ͽ9n%I$I$I$IK 6؁$I$I$IZ$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$RIӻAC2Y곷IJyy9?C<ΔI$IqJv9{|3W$I c=y`]tަ"I$I3+44$I6Cq)vI$%1`I~$InNh20%v$IX$W-oKy~Ѵ0~1$I6_vT-'rI<¼Hv$IXrFl:v)DZÀ.G1g;f4cv%'Zں$c}Ac~Z[N^~ >|9xvlTH<^| ƽw-Sߊ԰}^h˃QwSGv|)ԓU\ n£/‡c^dȦQ}aS瓏yq $IG[vwwV3~Ae2Z{Iy9p_6'_IR ek\s /|3?cr@6G]̅>e_їJ\s0tH8^~bz=;GNugާ)^Dzʍ#/d`lu_tcz|6dzq5G^P2IۻMS^r.YM@ӻ{D_YNfY4L23itZEÜ"CHiL*IY\Vsʠ!=.#˞Îk'ἡMʦwu*F *2.p t=͐r9٭ޝsAqLz_}6fB n,x|u$'-.ӷ9zܦ#=Xr?<{%z4`~Я8.?Pz<;\Ȑ#ZląK@~r_#N?y~DNie1*-għyؿ)('p>|"vKz]_nߥSQv !^'";>/n]{>k?"V_x/3;m]{+}W2Gy|/C)MX<QGwmq#vbw3zV)9R>5e/ZlשH;gx_jPKY}H:!EK(Ml ˌ~ Z}(/͈("\x3$I6 S&VCy "-62 j'c̙Zr*~!׽̴8zpkKu"Ir;I/LVnxuVlVvٝn?0eaHvmș7٫Ϗ3k^:m5!6= kfd1eZ^ۤu[wj3n^x֓ئdc^mfYw=;{מʦ`q׻.-ضbf=/Db sq0&>.އ/[FbƓz\`>}Lw ԃ$I7)A"R6g,2{򉌺Yc^gmWTSْ/=3qi˾^t)42jτtRK- ]wmȄs7ޛa=uԷJC!r*Y'0iB YK,2 V!,-yQm&wYN _5%c;o/"^ȓ$INRZ7 HH2JpnZ_sSߝx`T48#" V0h*Hyr}鶋q2E4ӡqF~ϥt-8>çwGp|zUV&(q=3{ӣs'>VU9Lj}XyY0Ynn6{D=2sf)movHE܈UT^L,g25&N,I|oFzǎsՌ.J p >9RR([9dL̏RV'qfΚOrسm6BBڴ!o7YAؽ֗1i/ZD}ϛüXuCcqH$90k~H׺k2^K)D&oL,[¸1SrlnNZ98 . X}Ǚ{,DN17P4o97^~>G_;I$ȓi &9SgOXМF֭,&K^$W؟ RiXN{%cL'r"]wώm*F}u~vMDG|oWz ީФn['#'XPO-c\uw>W }ލ,C˲p$mb$ KH[&gh6ڟE8S xg`ە#*E9$IүH]{Ց| ZػcpjcV}˸r25_: vCO󑷐ѯz"7x3w7)_r=MOxt$J>Dz[n>gwgnB YG\v#ip{{LK}U:{u!YvbQrM{03C^U0.>'Cp#V0&\$oNcOxrQ`+Ivc_/K=",#$m-;AU^^^=#r$q_W{0N[µN!I/)%WJ:na'H$IM,_i3n浯 %Iu%DJ4;B$I̔#D9?sS I~a)vƻgAnА~r:hI$WV^^e4;D$I~g$I$I$I$% $I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$%ͽM$I$I$I$m/-ج۷I$I$I$IFp hI$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$I$I$I$I$% `I$I$I$IJ$I$I$I$ %I$I$I$)IK$I$I$IR0$I$I$I$a,I$I$I$IIX$I$I$Ii<$I$I$IfXԝ[|yڞ4<`8c<6y?~6ꦰp둭7Bv^}R!+z+wOuK$I$I$*.Яr2c ҔC^g²m!ZM{o\$I$I$I`*Xx+ Z…ANW|GMD(]c6G߁ !+fר!ٞsngŅȢ-HmӃs.M/?dYNqaٮE*%_ɣ-É71Gىy>n\7FʀS[VbZ?4׭=-:Oڃd[:OȷfQ!I$I$IMdm} ot3ʲtU8>7wå{,ڳYlwu >KKrsuC۹`'$~l).doo+>̫͟Z|r|+RqH m^Z=9'&]|:yuipX&~"I$I$I1;f3cXP%+/g撛 R*rrR|CB  3}??=@u@t˽j21B: v ޏnS OIN;`ke%cłe$shְ".뽝pX>;t S?gwqC$I$I$I66ksQe6sqa,*zK<އէ^Jz}IіGJޭⅹmBnR$ /, dӬIr =k B  x~`7G?Gs"DXwéEd1_$6?%I$I$Ii 9ʾl`z_-1HپԂ_ 0azy& ~ K͜2IJ%,H~V!ۤ1e,ze(ǏL%ې]k݀&?s'r #oٝY "yWdҲ!\x73.yc{$I$I$I6A.f+$+3AيBcҸ ǝ());םAm3|:6UR/Q0é\|DkkUq Mߖڇv)dvEߣ;W+@ IDAT3 o}Hiss҃( W}!BHeE6I'#3X1WVݧ7.hJzrwrHM`ea9+c$I$I$Idr"OW3WO,cUb&6oZEx\ν.J)Y _?KBX#ؗ?TЉ9^[jcȼ /ۃߏcIuZnOσ(a4 hU:]+%Ԏ5F AmZ7 /A|y\So#`;Rg$pP&P}6Z:Ǐ>Z+];}]ݶAAWBݭR[3":%H _6guτp|y:$oЄ{14 =:J};9 \|'%OcO/|/NT&? #DKKil {《1U)=H ?H$I$Ic[SY̒,^;K݉O!pߴ8Nrwr33Y-K;EǼ,_/̏AF9>nD&Rٷ2+||V{xd.:PvnӀy큑<0n!)qOޣ39鬜4*W^@]N=r1pe=8 YvYi?ɭI]Tb-𠓙2yF_2sϧϾh`G1u(7~2Rhs4#V}(:, ِ/9{0"Z_ 6#LM!LI kBRԮ#X|:FG֝9$uF&ꄰy r#Fk9Z푵F '7:!p"uޏk> VaM[gSAm&]'Ub8aem/w{r9ؓ;MkªGRB8Yg*aa@AW1={_ZZ\r[&X8}br֍Wp,aCJݖ3?ӮU-e5W0?^,ϩ,Co䇍=Jb|h+ .JH_D7VMṆrДJgaR]Nc ^'fwLie@ EghS4L}.O (:qsvmHdXo{+7mm\}>̾b`1w=kb<8n-ܳoaQv\0j}r#G]w:muUg\5d]H;ۀ}f19Gu582oɟ//ZA2lwK?zߔz1gY`zm@bICX5^AW fC$F:YOpI>EߒWЂRpT~:3L"%G]w:Hg 'R-IJ!LIԔQ5f8 6°vԆ5Hplט9ݮH* ^; )avMZZ ^'lNaNZ7~Vwp&:yPgsݤ~`puLCֽIīʯ.omLN̸X)ŔnSJ+(ٽ3)>Ja-ItJYL2_A$5 ϒ(M[y1?:gMnBiqm{1saF d={'ίNfQ l;dL4Β`\2|_!QҤ jGM/a#Y2y'ǷshA{\gLY0&z7EzuWzS{wE@ BǓ(I0$f3gR׊3i3l W??i[*[w ֣kGô''}t)*Fw_M(9u$3>!vhJ8 :>ZSa{ 0Xm&ښhXF.x3KmâG|-AFsӬ_P&;97~Ka߹-V=<|9'ܣ<՗B?{K݋ghb䫾=ɻl4e9/QIQ>}=1c~T"[3gԕ :|tuYy 9ɿ:Z[ACV۟{7%fOIN6,r]NdvzdD1od/©*/F%4eM$|u4f[ᐋ~Zmd~Mz"Socq6aS6{`oe{s( Gx9oyd@! +)L]~.owO=2' Qb{zאwX uO6)N{V1iy ʋ!d ȤQ/:IkKlt~t^Bo'ة3vm,V\0n&sW$9LY+^coP5֭)kmN HU@4<kGDl x2`AP=6=ՋkB8ZUAjXAN[:WAMDu5Sgם:R@G댘W?<p!mK@u:_֑!lFQIlwɃ4=)zl4-N˿]@-Nk1V'=tumIZA>ݗi#bWtEuS;64iiU7U& p+fdߢf5k{ ۷eo@FB%} +Cx.ؕZwp=on9E:-M. TDMZ<rh0&ZQxу/{|Jwƥl?Y%d%x=[~]NP& - P9/eX捼-'SU; Jwl!+8w.k;m)a-q N懟zMwJ r^FxF;M', fF)cGʲ#"y%7×hрF#.#;&2~.LP;NɲAw3EsXf%etܒU;Da|l;襟?2(Δ;2ov^Eb5yPH:~o.'Xͨ\oDQ r;MlE yqMp2G߅q0 X'rS6 g̎ʨX4aZݧ|آ9]h%}O ?70Ҭ3ޑ dc(^Dqi %i g%ޥ+eQ ck [}OHI9ܜȪvIJEd,+vB"@%H;y d΍leOvhrݎ} NOϮyF; ӏcǦDҚy7蕇ꗼ^1w>J3aRr:qY?}>_s@lדYx679 2Jb?P ʝ&U#W4l;˶?;>*;3BH}ݵt " PP4Eqeu.c&!3f03[}9ǿ}`ӹ=u$3{j>-1- M?重ϋ禸-~28;yߖkiw&z{mVmע=jZSinnu6ilks;^oz'kN?Y<ŶrmǻCM$5[,95ٔoR_{2\DÂgI ]׽/mH{i-'am'`mm_EBXNo/mGg_d{z6W:T 0ppuket$L€1{XmEC>4Ձ6njOx>oʳ^ gޟ<:g^h.DG2^IkGzf)ί瓁=+~./%)ȼjL_V0hbUG1u5XF,;:758Ԭƈk4Nq/ +ÛtN¬sO-:'ذ6?߽ݩn׋m_#vq՗Ï?dvh}l86goh N7"""""""""^3o 9r7x ,yf_7 _L"xaV| =FU̹}&F]3󒱪nڼ>g*n9o*f]Gxa o7vAثP7/y8V?v\c;Mշo$t+ԺO;g_{O]N s t{o4Nlt+)㎜4|}޺o >m˄QpsKuxr|&uu{R] ._=w֧ <!:Yɯq/VƮY3:%$eun!%/|ٍAY$MXtϘ?Kc0ALNnhEydMُ7NeyO01p;齢!5% IDATk`n5kf`4."{#LB ~ k0|)Mqkc_wY{W l U¶m 6 mz@:Πbj&Eِ"tP#q[lVSAogtІňak nc ]U$~VYw`bq=?Uxh֞L]'u3*Isj)&ucԎCק;l9d÷1уsgdL ێډlNȦ1|8+r1{nkYGO-/^"?[u],9Gr2n}Ϩ0q*%>l~flg(`y_D7mKXeGM4|琻p]55^CxѼ5KI;O>?A R Gıl}6&ڷ:m7W݋4@6Jy]=D8q|%lf7ӉdΪ r1}HsX9ia7n=M>f?l _ہk2rE]׼q!`4/\X01 4nف5j;]ʪQGC:DDDDDDDDDDd""/>lԬTm`jH43MS(C' AkӶ(6Om4%vV;n6/d]?y44򽍐5[}} -_citb490 0w϶/d:h~sύ/ۘ:EDDDDDDDDDU,"^m\Q26n*R F*DNm9b7h\v(6k ,l#[[o/Ķަ0h7PZ8htb*@B -y a4LN6kbXjYկ 3k7wZNEDDDDDDDDDDa*JFUR] """"""""""A`vB`vB`vB`vB`vB`vB`vB`vB`s&4Ң u+*CGWcIgǂN!WC8~<97tpEp]MŜ28 O ߒPJчfOt7+>}=?g}#`sm7w4G3KzFPk.ud]mQ3q~>h<:?,|3PjU,Wbɜ,snꡏeQdz9ծ$6҃O 7McӯRoY6[5wy)c]tvLocGaY>9aݿ(rN훊z{_\DQcyjI<8d `/eDB?bp[\zeĎN<ҽD=7i/(q2n&sw^v6+:R4v= o"sKn /Uyt?ـs'bUt6QxRuFEM%it~w˖9S+Z0^[g}?Ɏwq_BVCGۅ WYFlƹal<= k:s&ih5%DVz~w1;1#L<s9sߖ|<~6M:cbEqP 1֝XMv-"kHfҧϪWPl:5v)3uo^l!x|$G'|avͬvᬛ'p  qZO8&7$qON!CwM]w s Wp"^K!CoOC[k|h?Oz lvyPvwml+{xAsPUG~-4+& E 9=~"w-dŴ]6=}(eOg8cˬծޭq`p cI5#9w x/t|JdT'NI?.w$=:߿s Dm|&$*5ث'˹et)=2%cA:e82*a=R!ߪgsGEļٟab'bu";贴mx2ƟFmR'<_Md%F2C/bpwɁ XUÓh ed2`˶{۸lg{}Aŵ^(p:fAŻ zzp{/X/FwV:α0> }5ԛXwf/"égp|=^*dyMK3aK(.1x"9x)5JGoG~v5a; s8"6#I/1/6Z;HR*s,z7ۗmwƳsK_^w`czIޗ r.ȃם'HЍqùhrQl:3>A떉4Μm] W;`̏8}Ӱ6rt[Dv}K>;wwj\qnbpUHh&*'ݯ`uiĔR\Abq:;Iu8 ev0??G";(4 nb%$?< {8sj +) ׇ5v/Zk*}ـa$S8e !gWnx\;Iz?ԃöY Rw9!t"r)=d#"䷸gG vCI)t}o;Z&FY7T;vMi\m: ŧo<<tP6v:XL ;zJϝKze /(y1I9v湬rʇږ*Ff&<؝/fHr?Agw{u6)pu:|W/ """""""̽_ܲ'da/ #;kEt=f 1y/7 lqf⯳㰘O0c 7ɇoچ/sk@_]Nжzy-o3N=Jg*闚Hb||S,컏gً>"_M׶Ya游752yT]m'OR zȺq"ڰ^OzY~Kp)t3&:{r>s2|A^;xo pvXBRiTMȩ-FE}H?!sC*E.ϑ㙬6EwTň^t7]69,ڜU4?t>3ay疿bi}H'6c͊Q l/FX/ exWD7qAu?v/o3cY{:86OR?6O=xҷ~=j۸n,׳gk^>|SFE;KSY5;#gM7]daWtV ?DAqS$mu;"t^_#$Դhc!~4q)8nyxe)M}q$Gu'~Ҋl<f_"q8Jpq ax"=vTcun5⪉cA7:ǎbLY lO, q+tt vZwynf|bl=3i&0oQ%IEGukVjm{~ۺ&џKK)u_=óy3&W,l-Fni\Oj4fLBneوSD/a't`(|/ 'G(A$qY=v|EmF0<1o nvž7>|81(ߟ77>xջkƸZRCz7'HsL_!9l0ܛ!s,jIu&L+eAWۆ;[,oraL1:)gR`6vii׌$$ek{+nlOSCI>X_=@'V^xߨjb_ǻ 6Kݨ貖ojFc}IBTXąOu~%.<ڟj$4] 'v<-bW>/vSkEl<#\QOXl=N1F-~ocyc齴a̫$W~w8=1b<J]ջH- S:jZ y}`H$)!*G75L,!)ʀ=tDv]0v_6+/XgSU\B}lXYfl$nYoW,'dwKub296o~jauI4eOKwsn-!cα8; }4$l"E- +x /H[ӟەzVs®_0IH]FJ+Mg.LL&|%]Q\ziSYzʹs3ًEdчZۄ%yjw,b6biX2_)d/ i MϾ!/H~'`% kջ_>K(mq{(r4bvWb5xXv`P;đ:}8;E{ف|6Vq3^L;xvz+.$EXmo[e≎o?[׈Y.QtS#(־2#>S[y0M9 Qx-Y"Š)O5a C4Xvf}1 rW{V}FUTj> CC @_cIP'2dQ帪}؍nQGaSo|ͥ~ڵ̢Bi4!"""""""pdSB~QܳҍmSN"-قfJvoE2nJN /OrR2:-Be\ )N NbBƖ&UPV)I dғ)+o࿺BBŇ偛ad$y)سOeƱbΝ~zvE8am`*شT+GGoq?eI|->_鹛c`s>W]I{siگk?!Tj2,ZTe()>0v^7"~NCu@$v+l+Pbtyo`I@l,V sab7̴voÀkF!m\>a0j/WŘ[bݕYCPn5+&\HԵ5fDYgP$z_]݃jWVZIdv=?"55 |Dⵝ0b+/">d ϐ8INmڹi;W^=[4m$s>{3${(FAQ6!->sVg,k[Oºk =Vx3?ܿβFbGg 29l4kn׋1Nghk6nGe-ޘp"OXW^QImd)3o#QX%UuUXhly.ؕʹj}<ut8䵟?W`z6ƫH҇cS2k4]ȕ^ #~+&Ugڞ|,aJ IDAT;5!S@?6.;/悃~zK֮䫅K8{Fp&2Adp,+g@ӕƑ3{pؗskn=詷y5Q_I >aڏ"޳[.YFg|²SGP hӇF:P&bc`,o+oWsPJ 擼E^ g=}Dq-1볩8 Tį!f^h.D=*nQ;0Rm#36 ֢>5IK0_dzh#C{`[XM!\4_pd#S3"u3[{} o8~,kή"sy6X0i<`j%#lv|={`a]8h5J];׎3Kq~=\FDA6U}>ƆD "Wⶓh4]WhQƹeG'G11cê׵/lkD1. 4xHB/c矲y\sL&Vr9-O$COrxYG^ș,l#e%F!p_~V+z禱qQHiݚD%وl="eSO\4֦ u0-i:UBSo̡ch(_K»/y_TYmvoja1k)=*O'mx@3ֆH~:]A~{`],w?ЃD &oF"}e'`Ȓ:lQ{PmoVնFȟF[ N…|9k"s]?_-?Ć_2?8#1sX\{nW/_iyv5/LA#yxjW'?A' g=/2*MO5~_'̺:=u /^gno FQz`*RJhP`ROQLҶ_ڸ9dبxf^2V]ϭ~E,YI3+IyމGld:qVLz /a6NC}="z-.%9<6?+j`s7(i/i],?=87[EٵۉMcpVVc~=KEFߓhL'mL7,$f6Ձ❄܆tSA ?%8ͣ۔mj'ô-uۜmYϏ= 7<<ؔ0cɥ{9ɹ?ۇM}F6>xD` Hg2[?ǣ4qx edN)d7? ׎MD j}H5#bm&]'dƆ )vmo&uƧl}=f̳<.uL(JZ;>KǣWnbGO;g_{O]N Z;|!2Fdլ0-/еioFRqA_[KH5W C:aG=cE+7eiaNomoOb݄,<̠:pY]>ÆM kՋt?#N'}HV>6,X|')3f],=l"hX&vypJ?dnlQH5)IMIs~oIcog#i68`I^Lvm9ia%GW1g'XưD#(Z ?m\mSuHw"}\_tDDDDDDDO8c9u#wL.Zk*}37 f奅a&E@־q,E[G@Qտc;;bmhz|\ߗS@@8؛A_z(K[}< OK^0Bc~E>ϸowկH*FMe۶;ƶ]v/?ŗmطE߳?i< !z$ߡv3Y59g_iEDDDDDDۧȟNMv2>"?99[[hٙsY)Q{Rچ<ȪO%&ܻo4ڏo_#6bFe m|5d޷xϊ~72-uֶ>ч7OxpfQ8$⯈iEWp5U|,fFu>>X`=G/E"""""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" ܜ츼>wy9|?4MrskVP<&Kp<βa~̠xR ?VNAtp㯅3Nrsx%;#(_ wuṬAa!adfx@xNApHH|zFZPrs?,18~DT$y915(~t?~OQpW 1Ip[ZrP@|\H̸rs)5)(~v >6$~n|,y9NI  _G^nÓׄ%CW&ƓUI!kᲄdDrs >6(~}?~^H|LJy91?+$~K ~zlp||Z2y956:(~{ ~JLp|bz y93?>:8~wTrs8::2(~o ~dTpQA:a!p@DxP|v'HN͡oxXPLWH̎_+(l?-$| A^n!WsN9͡?I^n+(N< Y)Ym[6y9'<7ZF@<$Y.W QH<"@7$w%r;!@R͐1]՜]/΁!9[ w=/*38+]}ÛrWp w]33rWhN;,($wi#9?iwkbH=$G5ƇO[BrYucH@|LJp@>$w]H_]BreM9-$w]]W%!kh w J Qr%!kx ~QH|d wFr!@̐@:-$wQ'I !:dž{Zir!9mJS iSVrځ9@QrWd #$t Gu]]]+'$wg+#$w3O]orZJHW %͹+8^ ]rWdHîJr? ͟.c>c>c>c>c>c>G>O}Lq:CbߋCO#EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD EDDDDDDDDDDDDD :'vD\)avoH}̎w` &8~o]&wN-91.{r:xCM cX$Q[DDDDDDDDDDDDG}D*wpk/tEq}J 9@il'3=$ǥ1=wQ`1"+,Wc:&ia֕sێ +1tÇi,-j^L$$s}0o-IPZCNb ^*:/pAF}  $/kQאS/Ow|mՔK={c$4s %H0@JRB ` %)!H 0@JRB ` %)!H 0@JRB ` %)!H 0@JRB ` %)!H 0@JRB ` %)!H 0@JR"w'xxXJpKxV~t|Ϻro5ojVڹ]H 0@JRB `R&If:y%IwMw]nIUcdt[ROO:~/i+XY^F},K 12:r$ĞyWNbt\.ño~{盭V,-.k㻻/~}}]`Nej,i1|?KE'_ <U B=ǰz#nz:31Q}==8<-T'bS8沩w6܌|!.68?xd2I׮Q*"Im4㘮]T׮8Z33quSF#V/hҵ]˗H2Gώ.-[.vmoEm{K#R `)!H 0@JRB `έNNF$qt[W6#I|Sn޺zz6Ni$.h6r+v3OIA`KK184lȿݼn:aY@YjBlw|~s3b)@Y=b Ʒ6coo;8<b:@ b? IDAT]fvf&Sŋ\L&XgL ]Vێ$IT,+W*?0p.h4br+=~z淼h: `H<|0@JRB `pn1Vo=Lܽ?Lҕ=ԙ7$qtd3;IsoL`A6+#4x劦J%VWlڃsalKl~奥\6w:==xǾw<֞๝s\ =x  |3\W'_>/-,z[+Wcan`:n:/֢XB5FmvPb'. .D>͍3LT'\6}}oo_\x-Үɩ}`:zu*fgv޵v$Ib>__kormyv.K$IԶk`t?[˝7xOӯ]۩?J2~djF=úSێ$¼&Aᣖ6t? ` %)!$;51Z.XeU'T[Q*^|ƍ22VLܽ?Lry ?q;#I3Y9dh,/-=RDS{58GVf<_YrskF~&}8y%0|[Fyx_OO1~[jGDMTciq1֙=T>yml?޺R  G.;f<^ּϒbia!6׏u^|䋶ԾGoo_>_9gݗb} s1^x?35ﳒ+ ykFDu2gWƫ٧(EPS[x1]'""VZDD  XDD|GBNϻ]}qpp{{o6FC־|>677GFbZݡXFx<;3ɩS=Ż?WaIcJ%um!1]q굫Wbv=s6h,..|]~# ǾVێ$IT,u՞y'>j5$w~y|Gwj۱ud|hJ\珌/-GDDur*67bsc#S˗W'߹7|hZ]5 h考Wq>?SފV׭C8  %)!H 0@J87Fw|$#qZ$q^q#\KRs'dd 8L\奅ϥ\J4S8Gq5k;HT,x劍 cXZ\fyrS^wviLLNR  G.Goo_>_\*Wcan-b}uboe \W6dqɘ}vf+\nߍ[/ ({b}}k쳨L,]=b K9 @G]|>me VW_O>:rmur*f==vX__r,/uٙNN?BNQիS1;m q7^W*"Im׺ 1R.G61`IDX9Pᣖ6p _ 냭$iE+ׇ[HNm;N4|_[;I}~|s+8wVy¼kx4V/ %)!H 0@J ~{ߏBpkw޹i]L )֨ף^FCӺRlwe4ޞuG-m~9-oލL&[1?LCRD 2?TRw %)!H 0@JRB ` %)!H 0@JRB ` %)!H 0@JR[۳_IENDB`mpire-2.10.2/docs/usage/mpire_dashboard_insights.png000066400000000000000000004411451461637447300225350ustar00rootroot00000000000000PNG  IHDR"zTXtRaw profile type exifxڭYrE /S"`|ԒBU%%AˢR<_;} >U/:>OC).({x]~Oi:__뷁lFyjR|^gY0 ?^r:~Ww S~&'PxN I̩U_O߲RG$% ǁ,?=׀ӝ|}_o[Y՝\iy-!O·#|Ꝥ|3 9 0b;*1NrcjLޑlDM-T$c.޷P ^_t|̚nxpaW"!Hn߿, seݏg!U[VG&:qZqoa2!_BP5XOgJA H\2 y7hFa0DN2FhP$YDTiK*H)E_פYEVmkJ-UkubKҴZܴ3r 8CF:hOg)L6+-pbRy.[wmCtSzY IZ#k\"匌ȸZ(h95r[+$2K`#y('|}ɛ[|gY9K/2c~el3}r7CֆThe4=yT>IZB*!Xd|V,Ǟe6V)uke1V]mRCR2]T s0moͲraBRrNDi{Cʏ?sjt ݀& CRʜQN}0EJ8bJ"7fN.aTP.eu(z=aL=BSdHhdq no:>Ub=$kx{ϵpF k,fygʖuiNHjFYkr܋6ΪK'i֠{ngisL'hIX6j75s {,{Ώ`be0Hɨ+ͽR6{3LOmI:殕^COeBOFS`G#,LwA Rvo C*63_x#Daf+OB(ԏ3ד"/YP]ɬs7j3PJ98}NHS2mQDi.iA/4=I\Jr'B0$cSoϐH΢6"iPX1TǾU[htop2˂`2T:L0|=FLⲅgIa+b$ p/p̬Xp47 &u04iӱ ;B`]?tzj!~Vv6v*h.X:p5:;ut;+6л(mu`4RDp>dLC4E0 -H <^)A74E3_3A}q^ːާ!j/$Pu@~]8^zhT 0' _nִ C>8lu7Ă݋D]'vK[&ÍL]u :%utL ?T$@ͽ?qC'%*iMF}~2#˴<eBq_B QJ-fVcE'|ta >p^W?wx^)'Xa[ Qw,AXr d(! JۺD#(&;9ꦦ$4TQ ;*Bl bE=B 'sv FO YCr> ?'v9@z<q|6ݖC(kM,`زb[l6SswN΍ jC"6Qp/-XlVݦ>YuTh xc2( Wu ] Zm+A.SpDPކ9LZ2n! \5 PbɱSkmhZLZ5f}< nҬ:*PeRu1N( *1@03ɒS@6'{&hr5ҵmK^d*_h!0OBC:ڄi"aӪ_%&s$Ҡie],.e&V B! K_iU8bv{*0ݻX"=Bt#jh5&vS4c10;XhM+p;.XybMfäQ͛z:ʸN2)Es!رH84x,,#=k$+ٶ#R qKv+a Z+v $mD`[X\Vgy]ũSwyg`@(>Ƀ v9=k{Ŭm4AiCd~MPfm?If+~({iԴa65PJ<(h,jC0Roi"J,Q⢛%N lߙH0ܡ",ltG"j CF|If]1 J1s1ھ,Xr>S݈H?ot]{)u؞x)8 7Wo t")F1EiW Hw(Vn)0[t0Q%FrhNabdTx5ip;ML1^!;`N)lKUo2XJCz.cHo+zc^w)&#>"i+ .0ϢK @P?9~§1۳dY+PĿWȥpU#X!Պ8b 'cXD3a BTI% Nb\XldVQ>&+~Ӷ 9س͠MxhZW&s|=Nrh}7V N3"f6쎥+b [T X;3jZ˙U;@a> pf~-i5KLo ۏ{ڧrDID>pe[#&>]@k~3M(MW .ZP\6!.2& giy zm@ @B\{9Jb'P Imڞ3m@omzR۰ѓ ,f-aè5J٦G8bID6'+#LBsa8ܺU L>ʿ!P+1ݚI1!+i.-&&XlvԧP ܽes7 bwď=Vaێ~`"lk΂.gf&OuPn T=f,+ ??:3 uw0h{  (AMG;oB ܝOE] iTXtXML:com.adobe.xmp "PiCCPICC profile(}=H@_SE* ␡:Y q*BZu0ChҐ8 ?.κ: "փ~{ӬQ@m3xEFc2YIJw1ܟGY 30muMOaEY%>'1ď\W<~\pY3#VZMx8j: U[R5_K\9 "TlhIxȥk(CwV~bK Ł@8ǎS?k'զ=uSSɐMٕ4|x?o@[c@Jpڼ;ۿgcr65{bKGDC pHYs  tIME `Ьc IDATxw|e&!{ 5Ҥ* .wvYO! HNBBH;#ɦ=?o^Kvgﴝ>cHjݬu\sRMaz߶m8u-& yCu#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!q d)U*W*#0m˛C(># zZ2dHs0 y<^$92 jhU:>3$C*W# Ir{<8D @&_q aaaaaz<.7^w?TP\GSmٲ>2HM5q۶-RZz$+>>^{v+**J1GuoAO=Y4}O[TT+Zv˲d%LԑÇ-jv.˲~u^?+_} z3턅([*TxYj~.0vv+@J.{+!!LTP`UD ժYK7|=ƎCKc6o?P WDR|y=Z*{ pq;eA6 OS^p P5I&L򙋾o.hH6l++Z[|Wm}?n VԨIcpC={AzGɛJzoS+#ujG-Ub)mܲU-SedM 8!!A%J$,YRUVh4ǝr{rT߭jU*+00P=O'O&鋯'h┟U|9=t_Vv}߯'Ndɒ኎aJz5*^ğ:i^qMӠ@*\:\lboq`)4ۢmytv?~/?Ð eNx/ccȿ+QS@2ض-۲eٶ,˒mJKKcC*K:sFTP!Pf59Zl$i xԹC;-_Vn[7j9 hE{d]$[ i:s,c5e,\Kr@,I"#ձS'IR&M5i_ .K.*UJNLhoaL2׶mtҵQTƛnJ=p0prdxvlJKGy72s̰ؐaᮝ'1.C9vvlX F* ru_qZ7Q  4Y |IR k-ھs;spDv?.\jש];w^r-׮]{ _-aa UHÇ+(X^=u#WիVn? 6 3+F,5kɶ2,8v2.!#8dbAfd3K mۖeٲ,|>K>ObEJ(\$L\NN$@|ry}qctw˖wy[ j߾nlNŊW|q͘6_GMS:uT^=|^_N'NTכۨ6jĄծ]G={V cvM4ICQ_ғO=u ݙCKYjJd\$5rٛ7;t(go{"I n?nԨƻejVHH$)zoyL=^Ԛ+nzy餥#ru3 Cn[%KTUZU{gj{|vSf-~yۧʪw;zԩZzzS F5T@`֯[b%KW ӌ3tS۶z|`=l>VϞ5 *'N{ϞZ0ޥ0 |dF[kƍ3fjUre=?|GS&ĄpC='|2yZlg}Nt!%&$h]jѪfL. 2MWvy5.7sIu uslfFRǵ{6leqlZ>LS3OV>`מz]v>O3.Ԍ /ڏeY=f_QcY xúu>2MCATAmڴ)W?͚7?\Lw҄ :|":N>|Y,OwmҒEuͷ讻0LըYCC^xA~VZ ׬s));g,Ү;/źnZ}4ieK3ro\۶FLt"4uj*o SN춚.Yŋ3X Vi:GI֭[T Hzo߾]sdԝ-[Ah\ApjU 8X׬}+rm&/Iڱc{[)gS;w} - IWѐ" ؗs[kHbJVZJ9\*RD;w.S/n3˔dHgsO$gU,Im-ٶYڙ e -s g6{ٲsݎpf$_@ 7nX{W.Kj/Z5WZh'Uʗ(ar4AP?XQQQR`Pׯ]j#4ds+jfKQF0L_ؾs@`` ٳɹsGN`bc|v{$IABgI )VL*UԼs2 T4qI՘[%˶d[lÔeZ᫭6ci25.lȱ.(kKRp7+f68jjmgVa=-V\1N[)ڶmuxx+4,Lzl§ɛqcʲ,RJ>}߽gO=;dذ~ΟA Ψ6iu oBjњի.O5szڽkEw^ٶKGŊn-ĄD*URw|^\㵳ʬ.SP#*,3ߠ~Fܰ<IƍƧC4o\u-"֮^ݻw9zڥ;_ڰa T]7zNyU酶JHH sw,VԾm*SƎs&:g^*b {wEWi~oߦzxZf:uO<իVQ*_$uDW2e?4]|-;W;~Vfvxsֿ} uA[Jf*])4#`p[>rA0pG7mRZZ~hÛߏ\O>fML2nӛC߼d'پ}Ǝݮ-[(=͚ͫ2yrߧ)gv͚|KΜ=/#[G\r'NPShF:wS7ofϜysۯ>}jkԷ=ޣ/Y ߏ'Tv7k۶7vRϧbJ:;m.)3GI^slYnA7}a9Bcρ1ꟕ3@ 3ߌR^ᬒN 85-MQ[iUթSWtiرO>^=s0T2ܹO˾vj]sTVU jUj<$IZti''۶4{,͞5}._|SkW^zFz၊ޣc02j,[._f<ϱ9`[Yi2LC pտmg38 #g;9S5 wv3L{zfU#mqvE]ׯ}:6ٽ[+WPDDn]"#tij&{쨎˕* :p\Ԣe+EEE)k;v>}=4`xkoݪ;vԩJMMSp`.]F BCUzu͝3G3~.I{Uײ|r.Y%8g*)3'?D9C q 8ap#*lofȖ/DڵV4_m6Xf'rlm[RΥ(@pT NQaa*\$r'ۭӟ;wrUi YB5_x劇 Ttq}wIC)ZTΥS\\vܥcTbBtW2J)\BgTo~;wY7op1fFvsy\'YlYYaϒ_EtFtRE9mڰQ#"t`_q6&M+ԭ^n/^t]-KO8ޭzZn˲qmܰᲇ9u=jq3+tUUKm8? ,HvG..2rmŭ֭[suѳBc8T|q۳Gi2YʨQ4==MQ|qG.!uVY'%)Ϸo0Ym,000000000000ѝ IDAT000000000000000000000000000000000000a&('q2!qfP548 A08 A08 A08 A08 A08 A08 A08 A08 A08 A08 A08 A08 A08 A08 A0fU,WHo[*Vk\Υc(Ѣ7nRpP+>k~zc/\oܫ,Լam&Vg'j/hM_\Mh{#k/Ԫ5}Ь؟xʶˀvkٻ%4,X~1[+)cmU) m4d8׵o6 #4yZ-#(puܳ_uVNWqf frzg"]=S[U$M\s?}Q7_fᩯ&,T恪zTAդ['hY2Y2KUd\9NyEZlm"uHjI|;MXY=.i:)u(I<1A{Wybf!Տh %7À$KgvЂdKޠ6nԠ7ҴaZÕlVv-Zlpy;YZ^ ٰB3c|*0B7uψ(Etfo"UUvV7pMk Sڒ`IGaR%WǒKh%^K54vOު|OuS2ঝu{S2LY?t)[zf'\oFA+2ߺ!Z-^ }1ߚ_E,cӏzJ$O%u|9=+h]hayK4Ӏ!SUT̓{j'-sMT֝DClY=ޱ*6GDNV xiv$>VO UJ.ݫ> ҙukwz.j]IruI2 )3z_kyJn;:\ݟO׻k`i_N|B{tǓOkuUŠօ_szQZ%~J7ֈje%_|,KpѴ 7#koG>3m|#IcQI׉Hk֤}S=բZxwFhHS5lw-BMց -PF |4+A;#jK%tO}6*ZPhVNf&M^wk~Gf|guVV->O{[FTl>Ry}\?|S~J5::!J1SS~wJNo g泝׆$Srlvg۵QƑ2tNC&hDC~tl:Ei/_fk.wӹzJ=zwMZ᪨:5 >>MÆ 4(~}j%*2Jڕzp}O],QOEN@զ4:Q)j >֔iǟ;d|?BLޡ35U>QӖ(ee%˫IۛеHCVK}T-n))?2>߫n, w4nFዞeV{?ia-!wI]Sǣf---W"B>Ej,27Lm^ٟGhʺ}C,o3HFjPڔm%+Lmm?kƴY>|w:K'kj7-}2J*1kͪxrƍթL+Fj_/F֜ubzkeL? w 5RWMcP8J7F)Y/='T}@|J7AXˮjSZ,єYpú.}p^*7=w*˦>ɻO3G~%Gz,DO}l]DITɰ^*A֎kS|p=8! mEu\OYkW,G-^_; oP ?й7NWbհks0fzCʋCʋNU/~mt~.e@uZRwUeӸ9P6ڪ©vfKk(m8p]]w<^xFOwvY׆fv֪ҷԇv1Fiw*W"JRtd(kl\vH{Jfح֞lmet2Uk4R=լZq6vI mc+7w*Y5덢!6c+6qeJf MXǕϦS@k9wzm{uld;nI2m^o|,Z7׿"K+Ǻ{*ZvUR1޿߭^+T7&ٟҥykiY{譏&h=G)5'[۫r%DnrHNsiI/o5O'˲ *RH.m;eFL3KlW8$Td1%NZqԾLVNj"=ԵqQ jk{/u>2YLYx#Na};RU%Lum+-Vgְo&kszQR!zg(MM4u?rnFeO9{Zֱzg Z3{15莞*UAiqw.N*Ple]n!PkuҞCžӇ䯽Yi=_:G]0vCNGk_k}:׫l_O_i@y*=)_ZjnudqS:~$Cj4Hh:VszkU)oUxyn(/#qZqܳ5}Hc>+:#]`Z:i/шx~jwկwD dU]k=\ʹEρ*n7KPfKʊm,w(<5Ul_T:I9TGw=w]u{>5r`uȨ2WzSssпe莦|NY4/zKo[HI%X\>KF.3]?tjh0DvջUmvM%ur\tSN%l [%V2Edgj&z@ч|2R"R,XY?^;O،` 2y$ȴʩB7LoZǴ|ziKa  iJIN%i(i^XN<ج( 3ܨ% %&+Xҹ˘vҥ D̬^=&QSeKG$%YTK%äzT7u߷ir*9}ǽzYz&ˢ\W˹c8qCzm9$otېid\vz۝}ZI*]4dWim2)&,磿ָR̭y?R@H%oI=G9H#U-?|@tȳ,^R C=n$CC*WN )"Yq:1GM QGG ]}}JW|%%ρ:P4HظVQZ?vT 혰EkwoЗ䮭.jcRRqwz޹&}G>6-wq^;i#gkt۷57׼В8mwQ(_}Mcݐ>Ymߞ-|*:c_7,UjD Pc}z6b0mD=[ϿK=yZB\ajyznz-CId^մ߿ĉVzZzF[vrUPj҉R@լ쒕;"UVU*4*VUO !;]?ϞT[IzيTr)~V59RJaMoҽV{ٔ\f.ZLZו:-+糥A 4$)H!łedte4Gn"?Qwgi5~t_>lۖ ZJWjjr/WTFy:vs*oONSqL-u-r!lۖ?*iؽ~J'T@Ϗ*|S-?Nm;s~|U{u[N 'm%ʨGN5\ R"w~nK??\8}sl",2LOfR*y_+Ը-ߜpթK2K(춮 ^] 3:|R-Xw˝CJG_# pIGAETUWl Yy^}NިE_ix2eguiQ;4h?Kӽڇ %_Tѫ=~Қ,Z]d.8r:Ky~egR7iQz:-*{9S8zf~S] }Vh=_I)FhV n9ɊӑXzm>~CȺ6<r+0P%KdxdةJIe8>WjŶV~4/]ZTg7~;RI>[4꣟jt7|ziTwUjq_XpUs`ԗNEbWit>|"}azKaDy߾.=hVJRjMժKik%9Mi}L2P@kUkmcw'+-W՛5MH tӽܣbfk_ԭ?~#u7w+xca> J[5POmшC3nuqbuԺK;R=Oo>_ۆ~m۩G/nw0H%kTSs稩j5+I?Oiϵ`1uS #몬ZUOk;hv@ޤ_@?s4oE>rR~xS/[Cj7RRRӍ{r'xzfNI:f^l[PJlsmgެ*y8\Ɖz5kھz*g|c o]:vC=hR5S=$E{%'5ZPXYu}*п'F|A9euIݤ5șd5j)wxTwwS;A"M X^bW, @{Q +UDEPkPWa} 93vkFPxUp*)Ϭ/2? Z qť-~nxV7:לIw Zm v<<޻>E:rxqlbĈ:1c{XQiO?a7L·oDwiÑagxq2s/Ӊ~aaDnz6w&6}zǻqhޡ<>M/O$|&ڋ^M!p{L2XN]X2h󯁙b_Ѹ%Tۖ1htv^}rӮ!Tz$*$(RfpPLf1 sßDr-lZ$I$œp:mmB҂3Z\ /+].=$I\N9qyÏ0 VMP0ݎZFr8fV$I$"^gz7:폪 ;X$Iuk׭U!iG3{"Ҩ?5{V]ǸϞƱ$I$I &M\COC9eHeмG$_$/w͚6}&L\ ^vEOt1={3=kUH$IN*$Iͼ̶$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IR1$I$I$I8c,I$I$I$Iq X$I$I$IA$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IRI YJ5)I$I$I$I+/srE$I$I$I$řbEp<&$I$I$I`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IR1$I$I$I8c,I$I$I$Iq X$I$I$IA$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)$X8U/gtFeI_:$F^yX!$I\,iUֺK %I[rr2k >Aƻ[!$I %mU/7$Ia\|yV$I$`ImtFe+A$In I$I* Xvs5$IcHNN$I$I%*t#?y 5LCU80jP~.5L,8߆ ĠofB[pՐ/rL=6Uh{|8x0?|18Z vlI$x6n: R o+|VEBe.0;8 O*bJGiJJ⇟3|@޾c[J7>:_Wv–`\N)>Kos.-aܺq:V;zUz0|;NjD,ӟ^x4"RP2yΟaM>Tfؗ13٧BhDžԆtg0}]҆9j":{} ;!=NcꓲmqcZ鯉2 yHJ}1>þ|mOD[v^˱7GS;2iwZ4+ณ;qɝ8h޼&=م o;x}VpΣ~ps&|{ !HڋwM/o#뿢9sޛCiҺ~zOne}%틛93nPν2(SPCߤ9/Կ6N(68z#>̖s)u\.oX['> u(#•ak~ 8 'Bą`njRp{֭$IAn#󚆌s;^MuGpuQ9mHފ|+*ɰ8TW^E6f"Tt{W +d#.ӝƿ?QĢ67rǩuS^\T{n=89Fލ=u(j[jNhw`ωrqR_ޓsmj CsU I*a#=hᚷ1mJaOr)7ir'7p5>΄r9bM4H[[D~v75viJ}Mܖi!vj͝91mұv1*>O+=ͣgλOSoNpggkNIR6 {IxKxyϥ"+?DVǬx)d7yAk?0PX;HQo`&zYҟn#;lan!C}zKH=j?am_)T~eqN>sڤ*R1wZj$BBT,C[?.$!mS0x}T9p#mBB$}>'3â_>dД4iTP)m-qmk[k/+KCʴx6Fҟy<6|5M];8ˢx:NgӟTjڄy+|s};۔X˛f,gכ3v Yd(DjשAuDڷ?fЋS{xΈn )Ӻ}s.锟?9]ݥ 5wnx>gTݥeJ-%\W1kA8`ݼy Q&oo]φ?#߁ cBZԆ+i0 K`<_]VBɰGkh4fZ$iy?Z0Q|[#jx\ [6yyW;Rqb ;yĀwE8` ?ʡWнgޜFi,Llz/s ▻?.0rƿolRQ<׻?#׿KKVՙ>q`l_=q/o} fm&y$fG4n8]jJYT?N^{]>~\58_V+i)n?s~ĵTr4kC}6?>z^ew:|q!DrbFc5Pa eias WkA.f5JikɥlK_+o*L;n<$Rw7v͞ͺo;Oskr,]iV.#+ZMt8v^9s/c#9Es1q^8o=+w|7; T':ZDHL*ӾLf9- $)mOgm))!&HNI&JJn6ٱ ΅dRR7[r82)dlx 7ܤ 󐤿ɇCW`B>"-d f)ᩳ$u?8s^4;AN0;u$IgVLŰ KD9Y7d&돌YP+Xj+s} wjߧvFB kR4[:a#Nvo~)j)r}VT(N9 Jk%%Ӡ-~O<-+S)VNouv{.mK)2m$duw:Gʇ(z l(}N:v'^ŋ++(֎ ѩ<.;{02 9T GHtohmA~\yET1^-6?nؖVkbSv_dyHBt(_}{rͣߓrrX˭KV&JnnY "*#@@~nކ4 X"L};ͣu7x# Ȟ;?GӶ: `M{dlDr2dgedX)IMfoˊ咝DJ҆BI"dž"b&w C$ȉšIphc >&É|@09tυv=GMX$J_WR9IF(8fTk'Gpnԧ_vV8O\ξQ)Ss7p%Z^r?Os- '7C9l#}G2fGhWOH}q ojضPkbZoZJ˘(J7 Ӹ+cNdhBe=LOLJ>3pыG3EP#<__v'V IIr =\$®bwYMX9w.kjեN2H-b e.W# sc@ud<E7{[N,Ӷn9BcWlz:Va N.+A\CXbX$/Ěp[UshVpZe`U-I)+#9Sc:$1l \%0r[{RږԢ/0+J 55B.<>֋ƿYɖ+eo  ]KяpS?8m 1onrˣal^<@uKXds2%/ ;ް*(u=0d^t?KnAem6zllP#G.)FTW.9 ;W{Iee%ݭ;o*3}y&υ<`+-0іƍmHes,m{ʴ3p<<$k\7#R8(bn `g3q9֨, )u<)Lg̈́ ,lz2NjBY׍s{_/:'tnz]"NCb3nuKλ 5duJ:/?A/oP:)XF'q)?\ND*0d4[C&ҬLR=,Z$b8`hWRG_<12 ) Z 98 LP2$a 0{,Iw#L[sՉeƷ߲Mg.اI)uhщ7:(84hy~ز/stA:_Ѕs٣yxe3kӰ\y--7ж $qWλ'Ox>oM%RիQB2 e3 b^R߫RR̜3>c6J_~oS>@:sƞ懹[63 5c rL@zs=D7ydMh/PZ͝Op^'ZTL RnW:^t G|o9l@ z{!-k [q{1sQu}:/CT^իR)%\z[+mئWk6-NKCVg.GРLmc:*`\S X9qzK2#ncOwnIY?-0xi|}n=.Jaф!=7УTAs o2Io{@~ |s # .[߆>X~W<NS;ènx7:$IRׅ빫u2K離s[9BE Wߡd[8NG^u}zhb,_*k rXh)ݶ_#ZJ; W8K{ȸn[Hiqw]NyN6IJiTjޛw:ny}9!m 9](R'>k0W0#q:`$ϵ=0K͙nء mw>XƵ=g"5Pc|omkfl_=xU{9fYMB>=Ȟt/n&z6?}>[윣6rirR'ZWNh_|~{\.wfZRǍ̿5[4V0T eswvZ!8Fv§_WIwVj6yG[*|CobcS?1𡓨c$I$I$IAUV⢑Ify{\"?.#=DB4le;YO$I$I$I_QEV$A&DXd {Ç/cbev=KAV"v.>yg?Dfsy=HInc/1a\|щiX1VO9$5ȵ'͠ǘYQ+nst37?CƬ l$I$I$I.nL:WˢWo9Lkp&F(\.Daws~s9릏> NpPgF!0$y7Х~zgn=]3BpuΤgy}:&n8?p˿=" 9 i8i.?\N>f؛K:f8.)_x.teۉ$I$I$IvrENne:m_zҡЄCL'?`LF0 в=|-gy>0[^њ_Ff僻c`քX Y֒PU9l?#YOG/ v"I$I$I,IglرehxsXְp,2B A/{ӏKzѵrcso㲂ݨi/Lq8_MHԵ\_%,T"ZsaJPЊiTS2BTlr<چ+Xp6seBlvA0Qf|N]94f'_+ l'$I$I$Iځt%>gno*rl9%DSF?r<\=9kI /ͽs{/7̒j 22SJLXaիV7Gz\cINͤ5,Bb ˵WŴo/r1*YM?ny' h}ƕ\ߵ#]I$I$I$m;ZrTױrm.A(:Af D"BqM.c9k֭[GAfOOxlڙl67f:UIHەjOzabwzulL ZpsH *_jHN)CPkV#0# '5wu?]ˆ_Qud>$I$I$I>,` أ+7<2,VŰa9v-ƾ㭇ߠW[fpȧa1uo;ڢ5Io= T_Ĩ!GjA #}pɅٕH[s狣Y@hHFp1=J>/pLN&+ݷ86`&1_^}^ȝ/EɘO=i dI$I$I$i[?;r!T3%I$I$I$b|k})'n>q֭%I$I$I$)K$I$I$IR1$IPJ IDAT$I$I8c,I$I$I$Iq X$I$I$IA$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IR1$I$I$I8c,I$I$I$Iq X$I$I$IA$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IR1$I$I$I8c,I$I$I$Iq X$I$I$IA$I$I$I$`I$I$I$I3$I$I$Ig %I$I$I$)K$I$I$IR1$I$I$I8`tT^KVXf)C A P Tx{8\xPQ 1/BAcAlt DP}PP6Q֢\+ TW Z_b'X_? "=*M 6As³Q챂?b+X@83-I$I$IT־\-Q [Ƿ^D Qx|o9ɯwO]/|y>E-7 }j#DV4,3;sb;$D HxC[ 8!! ևXQq`v}hZ/Ć`3`}L`v6 7 08`y/l.-|&~xȻQ(V6/g,X!F$I$I w6*yf^P, dIʑaċO2'ߧaHl^%Hn9:v|V/M"=Ђ_B}=x%dYc˲oTHcxI wѬݖ>gJJ⥼x{=+Tt8HB |5^XJyڏ;*ʙ _kÌTݗs.?cAҺ%d2kf"=Jv,<f ^I rgrxj$dgWyԻAk[2Ѯ<\mn+lm=bڑkhk"7-!df}^#TJPGme\ W Eap ,mXU[< mFۢ;błn08!hJPQ9Y&B(. cAQ]Ogêbp DxEph +ɆO񟭯Xa/ޞ"9LV|K)J;=iK_bK? 8䧷`mT\TfȞ$W$h,~+ail#'9-L}7 $G3KoiMBI ʥg싙aadzz?і/f /΋SwFDK8H,QGTZ@=UCbcc"5"ѱL-<(?}/$m3H|MV:i!+h:9{3<tګ-6مzTl'I$I$)NDj۸\6%GS1o0ek3H} _$KsC=9͏}ENju"A\yt_~z Nbyk[wj@!pʉ8Kitg,kk-3'7tTxwM+O=졏QcUmk/bMͪ&O #511 $QgiROf([9GVaUC^+OƩ2}rJ%G?  ҙsNSeOH HH. O7:\b{x0Ů1~*`6ʍ6\v}Zb{p \XEapÔX>ݰ9Vr5p,V,s At0XyJ_I]|!0E{ h!L;h.20!Ji3uaX|җayj䏦" ٞ{ʵ."80&+fھiO_J~얝pb )/oX{} k&P߁2}*U7]Y"8hg6ggzW//,k9dThלּՅw,'Fϳ' gai=g*Γpb*̮LmF8D;<92ì9^&TVOAx{ʔ68D0%,QuiU"8VNhdVK'Oل7Zkq ?:}"1<3 o ~kKACK@vVZn,X Hbk~R~+ 6g]gv-s/aQȟEV^k_WNՐw ?`=OebqSRc,([Țca霗8ީD2tM 'AZMPX~yF>Nf;ނ6U]x\2ķqrqN MͣhBpASGD٭dy%nd~/s8}6{m%yX7&_EoSIv$I$I} w6 yk/dP_8&2yd̛V9L8%mL޿(1oe1hBPu[OH@֨7yg=tŢ(fƈ=-n_*aXOpX @*K}TYWJi3b1hԀȳwrA+<~3-7 %UotbZƢ 6Lk͔s+QX{].GPH 6j)>0qR;h(1*ыngN-Y͖}|f tB,l_!|]_G huțy#oǎPo⏰יLg*{2,~&;ߚC( 몮`>nȴGpvYIo^yu ?fUYQw=H#P'K~wޭ]\,/k79=CKO M3q9Oojǘ>|q0i%ʐW.&tB2HJ.On$_&ek [=9FYDe1 Pl2e\Nr~iK{؛A=?p33[ٸ ڙ2X1 }PE浥X瀑C iy*[(צ?s'uvaipل&~GSi$I$I= w6~xM0n9IyYd=GBDB._ Ydͼ|t DM"DJid-[Nf|j˗!ry¥<6[;*N;ҨK*]H< 8Y1 b[^e"e]}xA^T R XA ..4dej>fV4ˏFɪ۰j<*X4cC`$O.x37l mqE'X4VULk\^Tٕb붇 )z1ˋkedU|x{Xgu ;T|a$1RǏl.ZL2 XYƬ*%㩜q T$ yrHX,"YDjҘ׷]aBlNuL/4S3Tb+uв ux/Lx B1B0Aav 1BE[W ^_a8BĀpA[;)xXLQ@\Kne]zA,\Ɇm ]P5pXa`08.Y>dS\t ,AQkx< ˊlsmGSmQR~<N'ʌ_hs g7B` ۘUuSRLYHt_*ˬ:|.۰M^/(;*aQt({Mۊ GS%.ZB$,Pƻcr24f:G6$?4OFrVoRd_;>jgv7e{hCR{-WvkGQ^AE b/?b齅PC !{۝Ȇ"}^73g933ϙ8, gPk='zG"2 UwO 4; շkVѝ̶cPHyX=i3LJCSh;yY\%}I%5ߵwbwҬyinľ >ƒ0c0!n0g5Wx?,Frpͭitzhzdɫn7[o'|jn,tFib9|pT%v48jf~6.;mθ/YuJ[cxb} +Fw -&Aދ]+HXMM OMoZًV IIqS1 [7Cn[hOn>DEG`#Nb"/ÊM~>yKxXV!֞~n!QD: `>M;bvIK}Mo;b}"°ߩ]3|/%&b+[:_mv6m bgĬpYب rRٯ0 , 1j/\+\[vvV&GUXkYջSS,v1WvU=kOcfQ]ݗw&:d.gw4Wu/_f`Y$၏2n,_s5/v74~5AG1Ƚ=9-}A)G:Wܵ`uuZcs)#]˙ôP\1j[z:oLB(CO..'xh XnPvm /CW7bMG3iq0V=u )k I/Fj︝< Wu{ !c?o(̧wQxD]/&z@p'hڎ=Cb}$gtRևiw#4୼rT'pIY(^KәciIA`g 3 8o,N(z\z.}{i+Y_ѝh_-k*aą86I[/b&8zx#blֺ';DղVuf {64?XVA{Oh0Dvla/<wvV>`M!8g[2:bڎ0 \; O#_'ԙ]Oأ߫ XK"A8|rUf`aw=%;C#,k&[O7CNg F\Ԇ+I?֘ {N oghm; GV!Jvh2{&[[?r AiאܭA8e?m>I;V4 nN)55̪wwPvkvck0M w+ٚ{YSfvWp{~0+QMlzQգت)ϨfUۯ^2|vC־UieͺE>fO[f}2R/iב]3)S6ޛ-=`^3z }kic.I0쿡~7fV&έɸR$[+{2sk/1f2\oAuly 'i`b/,씴mMymӉW _A#of=A`jzf 3lNaj2A.YWW7V)M4oQ(SP(F1>% 0aaKp8*pa;^`EM]YI 0||o?d`h=IwL%}uڦVevh lλ4W`\ -H mv*D(E4}i~nzc%/f"""""""""f{?u_:Rri({NؚⷸܺrܻlsSc|X^>ý,Ke+Y>4\\ϲUK ZynUrz:nǪ :̮ZZ55~\u[gIj{=ZZIWwe!+6Pkp:#|,2?Hu8?QqFQmt%"""""""""deeW\XT>=i9|Zw%h݂w?]+1լZw+Ǫ\'YquZbZvZACV%-2[Of k2n:xӶZV5K6~`~,\+pl&uya}tln Q52k2۲mkeO5KQK;MP=EQF `zVYU`E%8|q@ff ceuV!К@06lZ6O*^k{C keW׬_R֕jޖ^Sp5_kMuheݕ[U(FW VU7Y;:LrFRU !4Z6FUF2+][+*v6Y+ZKMRYCޡ\DNn'ld [UKCWl@U}kBWn 4 B5e"""""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" """""""""""""" ""e&PKHã@h5ʧa.q {_VL+yq ׷ckq)3߼>ie|tSj6"ZOwDDDDDDDDDD/G`[2s_w}q=)[_x$o-NO˨߫F_`sNF{O6y7~ 8L5Dҁί/cY֫$;@GYyi+:4jwɽW80SS)VSxXxOZYdFz^ ?i? |Dff'4 %49^ؔѳn>V__@;y咽Cg+whXp#9u獓*= is0\և![Yo0F=ͨs1X ~Ζ5|WhCQ2R`ؗkrMx +,H~ň 5aϱɥ<3ңK, }9o AH3|N Fxu.o\ ]&e?څ1W=#:[OPt0.e>ΝM˭lC-#ϯ8+ޚL": 9tdCEL+2ϸ͓Vx.Q%%[qg8y`8h4`89!e,}cw/{~_41;-'?^ 6ܶ_ߧ{t!RgXjn :F|N1la2BΪg[)5+x?t9[W]h?ݽMgQZH d3Fn0BȾ| =N##+*hBքg į2pmݸ9FA_?7O_бK&`91dk)?ve='%L~ g-ǮSk`*MVr}/ҏpȈ`5a~Ɂrd,~F%<?s&2dI=fR>i<&}C jt:wEaDcWM7yX)8*#[Ji2)Zo~cn?`5らsFTİ_.8=լ=ű|4]ڃ ;%鲎en|\T`vEĥPֈN]w8k/mGyϺxny/ϠH _IHDX4H4nFck}i4ibx'Fj;'k*%)=qq'\IRϭti]!JEosRovƖ Oֿ5)#=#͵;acNN^]}.csڄҐ\>x,$gN̜%BƁ}/;9Zc=^I& ~d S(7*' 4ɲ(? HS񟑤glێ폏ag=lԟm#  D3v=NǾlul"ꇵױֿY[ˆc[Ϳ]LXqܘ-Y3x'Fe@Aֹ<ʙ'#S{L֕)vstJ֕;:㹛d) mj#f^yޯ'aϖ/"^~A"4z!%4ɾ| ;z`+c^>lfg+3}i1YbÆ;4ȢKvgl)Mh/)m=w1–gQd31nB>yҏoJ%[20 wJ9 C2OK" tnc~vol(6fۈfNOƲ| "G¥$I>$"e9E+TWY,-g,縃@67>s ;z{#AݽQ{Imi:pl\*[߆[cH#O RoWyDYX*"0V\:vZO~9e'4K?!&=9v:ﭼzV=Pj]Oq>K)??岯~C)˦臉˶pT_~{+D top4OWaD9!AT2LItI[r-/!n+ds:+sҌ|"c_fSc˨g۵u?sKn{Cis◥?~YۍSRYz:62(dsYr`B^^w//˿;-Xot?dz6|8:f>'{YV>}g|:>0Ssx z7i~b] )i/ξ8 (?ڛ.TJR5t#D'1Ђ•fsn@K)[4r3ͪhaVf0FhcZF%+/0b`+u,Gڠ R ޅo2'OdEu71)>p7=6 =$N}?PE:g{tz6#" <|~;lє'IXvD>8?  |ȺYv/ X,{p`Kn `&`g$y[i3C򞍧9Gl'Owi?{d Cl8G;}z<?[;=s#GwދCʤ.֕"'4R@,OF p,zS@;Y:4=~/cKWMaʼn\V! 2ϵֈ}מ[wyE '’3*)]o]T!7 'm?89asr&zm7zX|ixH3 gz=AVM}-z>nR+Oa7)wNck:S(mUIJO>acVG0$& e?l_6n#ufczg!w:o3'470yEƏQs2HW؂KYۋFQ68pEL +DO=L̸f>6}N6#+8I&tc ۧy-,Ơaی3"Oa~ht+>aS |;BEu7қ݃i:0ĐzͻΫJ{+[GcӄtAWlz5aD:|OLJa3Zm[0YC?VKϲswDϩgN̥k2^ڄw+gmw42~M_I|>Vt]y_YT8ˈX>+Z*nnt'ϱߐܱہ8z_J`{˶48g_{܇+iv}eqIՇjӏ;ßw>ŞeQ?Kq4v3mi;"H}&f`IػJMJ'|3(wWZLuRƴ<;0uq5? 3砅2dU, 2) %tώ gbADyopͦ CWL532[3pe䋘Yd 6(JKjq`ǴUQBq 7nnçݕ|lvcG¬dIw&|t"x# ,<,j*, 8"Eǰgv θt8 HvbVb`ؾ-)s|a_h҉}=l#Bl,yNT`'?c|7xc mZ\_^`n5!e M̸pu'x(ЯNO&ֆ)cٶN:*ng}V8,(FyqGdul 0, oB,X"6xN-巏?w/Dd)|8qx5],[2j ɧͿms &6FpAA'!< +#!w694 uAxd9l&sX!K{1v|mGWV1+=c3gPkrzom.ҳZ2v_֓UFR|sbDž#;Y}Aq`*n`z;nh2p\KHLuD/[@A74I*J=}x& GSM[i1߭ T8ZM7=s$ǹmYy$**p7sNsbUsb1E# egi}ˉs&eGQwV`"l]Gh?[a r_F x3W=Ցч@cnß;Rlhc6JV\<()?TPbv'5kYң8d{uJ ݼpXu{7^Wkxe@}{[GWoV6BϢ -\5o^g8s&8ίDe*Ź?yfk 6OҵD\Nٿ<~FI=J>ψn̽,Xc0(=K< Bt@ 8: }V&~of֨9u͓]LnA);߻;>Wq8biLNaX%yViLύ0[d4QV>M|Έ~c )=/ ̩4]r߱-1Ղ} +t"99h4JSi6aU.}nUN;u![۰c<};ݶ`!qڭ99UǸp@16]?ztȍU?AGq*vG M"8aVa˨۾?ɠ)7u/o)ЙC^Y@ !eT*COT ڃB7(b}>Ku|͇l{D{1vǀ `<<چ7‰w~5Mn?YQ::TmĬ)"}h2ׇ{ΚY^ZL-:ս; W&ޛ:n36koNaTcShv"ٖB/qfTbs r?֣-UfYTw<`p)ӆ}E,|KpLz,'rd|̢z4akq)ϲHmy 1BRԱ Y=fg4&2 J;FyD%;ɉ {)0*vY]= q 2Ǫ# 6e=SLP}}քN"i`a>J߉h0R~$fTpE$6c"[% ep >at*N3V0 _ ?'kt1) ru|H2N:Ab#=Vn36M[q'tINV87WfZwTϴQl{?vlIV󀀗yESr1̮3[`߳ƟFyr#(=r޳Q֭[u*^LY)i3q$<9~1 APgv ?B{~z);[UVp0Lp%s(H}}ykIRlԃ涣v^A-y1}8h:B&1{${+;d lO)NBb(2/cow/zO=^NLhxoefp1(;|R:A`o6-S۳'fܽƪK5o{Wg#V9v犳?{wNsCۑzzc}'>m(1$wG\GS=ˍa?|Gs_:dN.j&콇{H䑃mZ󌈈߃20R~NYQq[7F0셩)< L~xxzﱉ疼Ĕa# 6Փ6z#ᗸɖױ*Tʗgވ;r¬\W|ïBvE<1#1&֭A)hsV GgvOiI ;Myߑ2S౧)}~㼹cJÍe6Xa({ 7pJʈS;;iAJ׬e ~çx3Atq=_JmIɴqfQ;<26*P!_"_oĿ;GId4]ouc"_;t<{7b&$+/dsO3s9=xtl3 OeXx+K=vA !hHVۋJ.`5=="?Mnds}oL ̍ۍ;QZ >:mSLc<DSVH ?oIl:{wr*"75`s)4h:֏|eܸwgyݍlyn2yq6.i1FV>y 8\vAy›& ,r>KTQGAQmg~Mi3Wc7}:Oh\G֋>ueޮikYz&7=3ˌBO/H 8]dl[+ce?`1\;e䇏6zNxbv$wzBKٵd%[rr|BNgP3-cҹh6=0  bve+vبhLEY~ENZOّS;#=bej҆>S2p_@ױsx_Bn@#k Mk=(bdpN3[̒ʹaBy X7Q]I@R:A%J][i{!/5gذ46 k+&/N-RlN6=;ۖ+ÆqY79_K[Y6{2Hu?l76?"fwY>4>&X)F]*qv9k!ջq&+p>4<~S!x&h7?wv?1*!#0pO듿@NƢt]\Kt})҃+ ?hR:V7%3""""""0eeeW\XT>=i.gM0IJ 0tSR~}ث{>O?"Tk7tqʩSևIπzaȹsOoaج7 ?w /9?,C߻a8pΧ]KDDDDDDDDDDD=+o 5o,ɟ aA 5GY?:ONPK8 jמx7TFH9WaĞ3Nbr:kNX/?ٙ_ԇNU/d2sJ"Coŏ7%e6u/?M| rNԂ9xqf1sdnf+3yu5n,ńb3ȷ&hsb [^utk/fc:n?c(ݿ/_}6-SBVY=$.>szjc|Gvސ-'q[<:uq$ euٱLeօupnfYyZ%/i!rZ'3z.Qʩxwp~D)ofS'Ak'o{<* ۟7Gf!gw %7\z5'eے,26}K^`eƕ3)7ҮmlݶH>!]8g nDgnz>FtƑmH|VVFDdsss熟G8;g&Og-Tq\OÇ|<i5(~2R.ҽ2W/oe9{~o/؛m`S/¯8F8}Nz҇OO/ez=~r&/u i}r?N`/%x_K#x'?o f dF$=U̳oHG!`|_*AmbcM!B!B!5ԞpO6e`^l)lO0DahlXl9fCu8~zHEUA aG:ښIYvh*߳X6eHޙp?UR>zGi{qVl+j% R˰r(.nY_Ҍ[崕 g\SAPҫc >Ws6jWoM*Ȫ%kmfEZ Ɖ/;Â}?Y)}~R^/^9J51btO"[Y""fcDK;Y8rj/?UUѴNz~=g@"MZ#6uA6kB!B!B! `q`8_݁hdܙ#x&LéY\j !̓QrȿYΕ+EV1W|fǁm?(k'7cSgXR|yLC]7&WlB\H(mZZL74<h#$=uWwL9+fC.j ^͵x_$}.9\d#S,糽!kQO=K) &Ha3g B\:-`p(!Ҥ,HbpWl( B!B!B!F#(֞W6P#X'⮿A~nf+ 5Bq{m mBζ+^Q W0o:ǞU Z})((%04/(Є3a7OsP@Bn84=/_x;kHP^kXӬ9)L/_M/`A=9%G>i;0<c|3>0O(U0|O*}· 4ggyxe!q|1~A Ǣ:l !B!B!'J#M JP?y_3tRs)sQbUSڷӑydTZaYZiт ?N}<;v \rvT KEG.s 5^+w:h6vA<95oK;j!v&F2aDxuGg@R%|ѓ_<9'bZQim^_} ڠ+ iNun]vTvɦǬƻ9scN0? ~J 1+#3 F׵̇W_C}\t3ٿ9 1RbS1L-I[-w:hi-Y W(#B!B!B(F#823A8z?|{I&6fx7^Ԑf''Kp{3xً(_:=%IVrӱ-(,?>~_>;9ZLݫQAZO2_SG08c)/-Oܹ2Ym9s<΋4_DkS$Za#0_8)rf3vETmc;W²{J1sU 4KG$ȚuLeN/~ŒehE??߯͡}ɓh>w^kiIvZIB!B!B!h `p#"=%< 萶*LJG2+"f&_}uG#}ݼbk=PST^{' ͚6?!|B!B!B!rsn3n=5߬ƿǟ<4D -M@~o!/EGHLme3mxÇPM`!B!B!BFhRWXC{23^峳`Ͼ oLze+97`S64B!B!B!~B!B!B!B4y4B!B!B!B۞l !B!B!B!D#B!B!B!BFB!B!B!B41,B!B!B!Ml !B!B!B!D#B!B!B!BFB!B!B!B41,B!B!B!Ml !B!B!B!D#B!B!B!BFB!B!B!B41,B!B!B!Ml !B!B!B!D#B!B!B!BFB!B!B!B41,B!B!B!Ml !B!B!B!D#B!B!B!BFB!B!B!B41,B!B!B!Ml !B!B!B!D#B!B!B!BFw` zinE_]Կo2zɘ / ()O3󈸵O~vM8nu5фLcm3z/B2HܙԀd4?ssnJ,&wEOɸ8m(6s#3CU#w5<_ z6;ݢ?'zU;'L<? IDAT|0z Ɲ\{}xr@`ˏUj8]o>oo6x^6\{^375Nch?7hcN 26]}} /X͌ufVeLZGDƩodU(:L'nnKBVhU@@)SZ.l{} ֱg͗l} z!ro|5nw=-VV8r8vj uo#a6ktKQoE7[p>-Kn_)]9>ϧ\(Ced;𺛍m94(хl[ G - _U%w5ZlujQL 1,2ʣ%&2:䩞o#O57<"~,T5=4v-o<հǽ֮W575t4m27n#&}}3N(jEy>o\' Qx& }` =Hz-߄q2z&ԓ{%F[R'! +@Q1;zcHwmgNCOb)0b(E90]>U9 \'؁.~8YwצTlFkZWs{kv! }3[3 r hK9amOr.{{bTU jx+.5S\Bc[א{|;{+ ߱P6V6`+HlO.z-bД A[t㙕rMDqLڑJQ)6N\c< H:5K.֯fsR * 2mloڅsdFs{N_{!c&0gk3wj .blGsWG1Nw[(pf%8Ս6+gvd@ЎSF?:ȪX4JǤh) aj;3~pzE-%VVNzV׆sdt_k#/(&"4X|wЎ鰜Ê5ȴV].b|uOmEoZz{=jw Ϙb/ı-kpJttnT-1HvSfB,i͠LKxƱ+jc8WtO+F=pݳD!Nbыsc@)4_ 8k79ymy)҆sؓ_`fNMt۱u'`C0Gxo94ĎITIܳofPZ1Q7 9II.2ogR Fcp?I.Z9|e|7z$ՙr)tqt:JK^UX KlLzo׳b*6z4 oDǒsn>'J`'&7Vqvwl;m՗)1̀2$gC\f2NJ89LJ4qF狷0KL=?J[,wڝԛBNCCs |f´l)3\ s9ǍcD5.m 27Qi~҆8cWc|0-;mqVndMOsg98V%لbv;~ݭ8G>tlȡL+ڊEkIoU[_w.;=|5g)a\\z ,jAsƽe SSњ6m[bi87Gs2M bZr 4#s=͜{Bi$3$hSd5}|Wοyr>.e=-J${FgUZ쩙g:\̩5vG8+!z)M| {.̛ۜJzNݣ#֧VT~pz0y0{cJ?AN~G,Y䤞k\J''>reFY.gxFtm gä5Lug~;OE\7k.{fЎDpfVHQ}T5~}\kKm3ulo7^ʾu!62~pLGhKiv#yuZh]=4\q8Er])ޡ:S:UzHaU֬m~6q4BW{o5=t=_kzZ&Y-G2sl\:HkIn^z:k7st=D7{ct+7Wpis8]|b'evD_~̈j׻_%Z#:=W1np9Ϻ[7ǔ),:LO1˸?;B&<@ԯTyhL}-G'*&ܮVKZ/p~+]'7ƚeh(kyc]v%O=JevM8c1XqM|,i{Ww-œF۫e4ۛϵ?{ikM0O~ODBY^ΗqhNs֦xy)7^~ĶLsCL8ś>歏6ʿkc$FK?f5ENffj*?ʯW|eV Lŋx8̈́|p'6r :P3>hūł-t9~]!v zy}y0?·8o/fezKE¶ ~}U?/Єg:c~-lΣٌla #azˬ^88p7ZbMkV뼴x%1<ڽb$ÿn%;r c;(rtbkO]UA'``}{ƿ?=@yϙ/ƄqS[;sPӋI¯ΓBE/^"5b4c:q75!6xx0MŅ&2}.k_~%G$.FcT@JH_6DM bIJÿdőeW?`ej3&C{cx;nSta 39xm"EX:7EFj,\{:ƉG㾁wc9+A 27W_͸t6^κ+};/_-[=z+S 9AJDVcE/`+DTfNy\SLiE UKdt >-ӓ8os7Sz}}W,?]{iZVO\R2BÁe`gl(ʽ;8pG)ۗKRhB4Kr,M,~oo|q(zW{6>y E愆65_W^'#b8\=yp%EmOy*ڢEGax#=nKMռ+v]g}](m#MU;|tHGd&%M=]Ib\1^N} ӺI"9Oüp1iޏYb/*Q<Ǣ~uTz2kvw+oh~FQAḿ/2$vx#Ym3u\lu!vLٛo8ccmZm&y{6)gf}YMh_L&k^l5Wꐛ<=Y5v44!cR^~vubkjyUs?5fNe57]Cڮwm~c9flСLLI f厌TWs-'^椖v^<#x^ih"ӀblCL_64S>饝_* u霭ݶWql[AiN^T6TZ`T,>G,OdKg 눻'fd[c-PNOX,قNGRThNJOfqxPi!dEdq<1PMր;tma6˦a%?a{.FгSM@m+vqb12]cˡLkߣ#;sьn3vǵ2G^*EAt ( !:s)sXODztiEn~ٹ"vK&ѥ+Zr лK$s|{:oJ9o?);K\ -gɵ:(9͖vo/?b?Ԩ};Mȶ)9(džށQ^-0=cim(YWdž1g!Y 젖epJ@)MdѫwNq|ݺ46TJױೝ7r3e~ >Pٜ>h#ҜxF1m4<|E\V;9AGzDika+6^c9wpW 9[BAqiR8iIӓXt?khٹ=چBV{m%<ӖƾI䕕Sr#!JD ÆJqV6L(\z J]}.q#20dĴkTݎtX?|}4!*.Тz>7 AL*`!iwq,}i%#9gndsxr,2tȡsOQQrMkev|_=Ŭ73.ح6O׻WMKu&~>Ηq/7*'H6w@>4rεyrXM\OԲW\UXmֺ-u(גG}F\X-.],K_KQb4\Er>#]ع7FNDx4W==IN6oOɎݒ-G)j׍~r,r=mNjQz\gNYj.a4(hnuU|0וƞ.92JıilugyknZ~d{R16{ ) >&U}׮TO%ٜ:w` Գ6s/w-~f6/Nwoݮ1_~_ϟŌWǁjN`Z}U{ɰ@zh2mf5=TH >m" JƆ;8[4bT7@Ѡ01ad㯀Gq77PTb~ V:m|e#z Fىٸ\\)uCU@J0+ .ŊfQz7פ2琙C%$%12&gbC^3~ՒwmP(.a(DG;1w:Ea;nb~C㤁xcgџa3KWBꉝw݂" ^cE뷈*͆MV\p39;0M$ L6y$l1A cZI |:t&b%Ҭ搫 ,&'ӹ! pPRbШZ(w92ų诡t"T_7bhE2lJA`jpSZU!.#72e~W ןSg߼%/^˱nuv$EBNIEdq /OUvSca0z-/1'cjbk~_e4cS1mSXtW+sj4x*bg;(R0)՝ Wٹ>p?Z-1XȺ\^pEqW.kKJJ*rɌWGyFQUYmU~Zګ8S|1zV\;bxm_%k2w]S[pך^g%^Laz)skW*sE;9hwM\Slǥ9vfzչ*cu8n;*`UMI'9kF⃜ʶӐzh26^ÚC2&%@NT !w.1mkN X;չ^>qB6ŭLJfXt`t1͛O _M >ɱ Sjy!GsCB`m$g wgv(2U!lأFgCƁx2Z?T4ş@ cP t5I7,R'0]+})V薛(^6?Wqa2[_+oBL|\0 c)2#.ZCx1hi *>>Q 6CΓ.Z"lZWf̖xk5g-hAi}{f}Z^+YrjܔŽ,;_(澉}8$e1z57AAdVM>t7@9DvgJAr %W '+u=YQ't@Re`S0! Јq1dRbR@Y}Gy`R?=DXNY}wtU,>Ni 4 4sb2vuɴ{3&5/ݞڀcRݟy]λ.z`zw[ƜtJ/'Z'HE p=j9=]|"}}CYr h((ڋ)2]_; Th֊bDzˎFWcY u;MXfoƙOa=؞nٖgrwD2?2>U79,[;:poxP6~}.^M t {}䲤k7+=JK(hp?0hvxuwMoU>wZk0SϪJx񅫿W0աZX,sM\S 9+Mc鑴`z>QoݳSZOy1xMgHdjs|z,ի.zzY][wP ɉٌkߓذR_İ}P"HXS5]LnګEۑ<N씞~U.5/tĚʑ͉W{uGs_ crs)V|ucd04 Z' V m9oK;]#T9/xvelO$RR#lv:#A~A„x#0{3|]?cdB:1_k^FR\2}G/M&'&*J3dEdBP5^wXd܆ENQ9j1F-W 'ϳ"X<0+:b6Ky6sdTQ;FjONAkbxxpx-n~vg2jd[44ƄA-,֋ysm*h^n څ@oE9rRHtep3/tZdp.FHQiVk fpz*]pRvrRwl%>xS DŚtXĄns7='s,+!^b\l^QsG(c[K.(|eZCH.ͤ۷+dTgz:+-돣0ᚺuв0z5Fۂ#{;G.jp"#'{\ɱTzeg6; .儆׊7>^w€(/4Zasٱ|+cpQH~nQ|hi0Z{c39}J!2h[3dPbVNڰ^cA@+Ć׺a>4`wCݗѝi DJi_tԱS9hdg⩾t]{+N1:)H1E3瓯ǝJAJ*=hM#Pد~g91 D BOgb3C/o42Lv2>Z4QXז`J>CZތ#P& EM?SWC囆i8Ch;3cF:t~o)4uךzVdIڑ^L}j>_+o:~8|=+X{#Iٲ-&16]`r:vk|/dP(Mзk9Z(TŇj/>ȟg(ϻU߰-^-߲axfL8V8@j56^n&+a7Hq9:'f v/Hy7jk~v>eW0y<ƒKґ|u ڹaoR| '_psxt,rG?4ë(}Ͼgj&t{:/RDکӜoݚjϽg)6HZ R/)tUI)E'x& S|^yk)vsn"ױ]WP<ȟf`8Śws^3KD+6]ՂVsN=(v՛9Qk$9}WvWRHq KydWwԬ0k`r|%!cAVn Y‹~=q\3"ׯ]e6qTp9?^T>ָj0mX< husv.߈~D~=eNL˛WW;OszR6~ 0)!>Gz3K>nSk^֮[3f2R y)Oh/E.kcq5ז`ŷL,']7pTJR=nc^ o{<)Hش&oƠ-NlF;5S{ö})g~2k2M`EI=Y5W3s WOSZUV uRDԡ0bӚ13pLS3=rlj6'*̔Q:8snA+ʴiVΑr5lOFr/1Amd\ϊ=T~ ̼7!FX @Kpq<1m Vɲ^!t3q=[UOp#0mb?:6VƁٞ\6'ɥ>b2 )>l;NgYq oDǒ/mз4~9! W+ȚJsuo#h }3[3ٱx]!c~:,b12 Iwy.gwgTk)qX̺ti5.?3.M%f<isnL(Lb~ AS΁uؒlBuOcWo.Ol@dV,ZKzڢ2 !B!B!░GCA|;`jt~g>xsjz1_N2BÁe`gl(ʽ;|;けv!bVdڜ~ѭy{UOK'qoR/}_ϥ6h<9JڟygX9vg3K+AѴwpocG+>ŲwWsPĴ0w­dGalg?y[_fsU3>hūł-t9~ |]ԉI߸Enӝ)O*6JW`Ӷka JaE*R#F3Ҝ'>4TM)eşE`)N,Axqg.'G :x)Fr^,r9+[OJ8ɞKi }wS|>~slq"8 k.o۝L7~>Wp VDDDDDDDDDDDFđMOij(O)4/VwnP d5*sv&zEWdv{:G׮gWeO#z/Oga =6\)W%mkٝ=3 +wนu9Rex$XŊT;4`gYr'8ቯ<:6r45}Dpa)In])y3ٸ2G0ByUJل,K#kXw2|0a1sjoh{B~gADL#f)1_0]y<3[3u>蛈uUP(Dl `J[T erP97w2,霌 M*E9qKojHC^t{WH޹ k% GDgɎ-ص }9lBm {A-]cـ X3+,nD O݋eډo9dpai!-5BLM!td\,5t#;% GjJ&n^Mɼlο0L"3b3aw`Xꁗ;{0zX9 q!xzy` jΨ^\cxO,$aDZ3TR2fz*Ɵk9Fn[,$- gxxah]pB$==؃NfJ[7y/#$6EpJЬ#Զ!Qr,/Z/n;C֍~-# x9AJlF-l3cW7yGHUF1b-&&$f ؖS٤0uX4o1y؏ѵa%)rx#H#= w/p =wGsI~SC1wrث98HÞyn^ %4VQgiв:N{#8͡xmI 73EkvbyԖ@!붠v V@6NsߧZgo/1 i揳F iY>Ƚ1γcw V~{MjjD/+͇3gM.Y{8Ԁp1 ܂ qv),fez5 1<})\d&d}qv&i]9ݸ-?Hd|0MfZ6m=1obnY"*ٰt=aK+sDIsOWw[WE$F>};}HvE2x۬xkBrN7k4޲>w[qJ^V2=tG$yRvOϚKp'& La}F3kq;N,G϶t= ⏱eBx=KӁْ́G8`.rOs]f/tw"'8f);gw$&l-< \9%mqrI:ܹb̤̞Fvy+fFe,}Ow ~5mC] N\j?JӭP9c۶S&>xk,½s;F?53ӵcR` B]~KU#y4)w$8v <̑!X7jsiמ :k,*?/Žk}֝ӻs47kvb˟;f8gؽp/G2?ڹ;h\quJ)Sq Q[I=[ˢ#5EDDDDDDDDDD_">>^ Gk.}Ϯ-Mhh9/ԗ;NܦM`Yz4cxBDDDDDDDDDDD䎧Fp`'rGD*."""""""""""" -"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,"""""""""""""RȨ,C IDAT:kME=06B9 "ֿ~ͨ,ȅ z26G~cMsű#9ݨH4i|؝Loyc m("""""/bP/.)\fYL\%5bǧ0RZ>/",2n\Gۏ[r )e>y)λfc0Qo)?ኗ7/OT򋈈ȿIΝ,V"ur8g:/eQm{ɺ>쉗{+G|\5#zc'Odsk9ڧ~2.=k@-O49y!'/ 2Ӣ~ C- 26 ffĒԉ໇2W}9p& Z= }0 /j7AmR#3ݿOwc,O6#ڠxY(EMR1gd̸ ~"Vٍk2J9lbOu^,p` ~ە L=,-EcGStQJ7Ҿ*%ۻtw?ϴ ,6B/"4\v;^IYz7^dzLy@Zɉ?_ͺ\\Q?][<@bxY28;dU"""""r{r j7roIi&me؝z+zIZe/aG+O'IFS5pw#f@E'C(g<Ƚ^`FBM_.6|rDC>54ՇO3D cYH^ZO<6_^A6 kYcs^^&?ZKN>yyGw2~pFAO{o<K kcz>Dgo<=d = <-TOۖ-> e/$ qZ_0a#yg߲4U=*AOZHZ8O~짖/{I[f0s{ 5 ey)~I%pם4l}2yYdPu+:`Ή<44 $u$<<𱤓I;͎Lr|+ѲMrmn|NQ 6}#qW`[ ؗmGyQ-筢rNlxӍ-뒵 fǓ4id*|7cϊ%DIý^Bi<V : 1W'w۞o .)Y3g $ͼ)""""" wSſ}5{9oƱkP7l]\q6. P\H3䔬.BpQ*6Qv"qֈ/=Aɤ\+v˸H@ BwOs4sd|A#oǶ0 OK&rLonti45d&ɿ3Z x][IP|n (JQR؛R`xye@?$}}IO(Չ'G{rP{ԫS)edKpX6|z8m畕+ ?}C8׆~0"+cw5jAp>9a3$Xɿ#lLʎB>zm!Fc9eO!1%Fsς2\/Cd̦Vns/Qk=!_H-l(NOy|JӰٕϮ""""""}4& jK,Yh:xr"[nֳte mAwy҉Rֲ!. Ls:Wl׌c]q7rIMJ' !wsX-6~Gp=}prdx6lG6iy䒛L@Pq|]pwwBJӻ+mج`G yV>^zq(mJ@N2gpemjpHc&}*wmGؙn/ٺ#k;l[׻з?VJ4Xz[Dn.d};}ݡVVJniR Fp|c 4nڮnfnT'7<d$aOEjJQ̀KG7vy7Fwx3d$۔[9o0H;ztl6ۖz&w>=!޲WXB<cX\qıock阝AFaVLB J8Cuqj1*Ƙ0i'iX _o[֝0q*uȄiL5,Lh?1ڴ᫗ȋC yg\ #n/EDDDD0rs7szn8J;Gcќ$eD^ĈQBz^iᙗ׉`&㛩M|:xf eyң#Y8~=BDnuktw?$G1qO?.3"=#+^.%[ȩ0w^wH8/F%""""")h\quJ)S"""""""""""""򷈏O/#-?3{ ߳kK=ZDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDDQ#XDDDDDDDDDDDDD)wVADD` j@Q#X;-Q (""""""BE`BF`BF`BF`BF`BF`BF`BF`BF`BF`B\)$\OG(DDDDDnO/JP_{qJ3Q'&}r1$qO}w ~3,]̟ÒkYawJZJܸR ,c'/FNb9,68?qSSDFWir!`^dgbo(W>7QtBz@9_'sdY|ٯ4V 65ʼ,;y;øq.Ы2yiT~3 ϶'\ g'OY+1b,?ɘMWf^Z2XbW^>e6?Ia ;gk~6> cJ]%?TU̔_]o/"mk}yW/˩S̟uN|R?FPv1뷴B߿c?OU9{A>m_3ym*\^Wb-R!?g~dKł+RH ~p?HjbI-Zazo\>ޅ#l^T6hCc}6K=P&%*XY9LHi{ yn$vm&*W{ZN,?mƣ}h6QϺ}2mQW/*F}9O6~a]'ǣ)"""ܿyR!?c?PjUͿ3FUOtB2gY˭lNlEqVV8I5M'й%5hRދw<8i+cx6i;gW# [][GU\,3֛ξꚿϜ3޸7tobQ^euEɸ*C+7:XFjLAAͪi0lAp+Mٴ2%=rݻ?4s†2uF9^9{*a]N\̗;EѾ=Sq~^k$GKWzѧtqTCFmX| iwJ_vSC,4W&/s<l2e 1]X?zp![u.|N洮K D|p |YFWI썘 Nxivp>й^0ɑ:}=\#˙M|ї,= 'U{Q#|Տɻ\j RŲ ku57E3Uc~9KmbNex~7<^v?iAY4g}зueFl8\NQ~Kyop3nsMj0 y蹥wVKrdi؂z悭XCDJdY=A 7/bb;6AcpG \Da h3>NK|J,~g w}]dܫ:QI^\[Sx3W=JpL$Wj>Ky,ti{_7ߢW~"W+9)"""W$b_ R)ĉj(iijaRVgP*} :>#ZS6{7 Ku{eya>kЄZX|9[:.Nh6T wwFm̛ϯ[Ua%bP<ֱ g0~RVӰ{dZeyXV"ɨśCeD-z8=5nM '|%mЧU5݀/?H0{&QySq,73ϲYg~ vB<3Ldqoj|.!'e-in]`cv(<]hb7 }ڕ쪙|{4=+p<{I7$% >L6:_xNF@+'uϰb6M;ង_uÛfЫ*y2_كl3ku^:;pY=sSm+w5oA|2iq%Z0TVԲ5|1e#k3aT=+_|ܐf_ Z䕡ZʔH-w?/eZݼƘ2ovѬx$9`yA%v3˹q@ݩnCkƲu Uy[HU< T-mRELl'jO-9G7̉CKf27ҟn]®yGrxk9vc% e,^Ns7~ٷm ?炮ސk.< ydPoFiD-| b/tqϮXItG^~m= }ɍX`^<ݚO9ݔwkWfk)TrMZ ||rV}ĶjGHqf)c+ɒJIi/y,[cnF3Y+WU^ddsZIwՊ%v p>"~vٲh^8|td(Gj?&?_W$JKO@pl{4(yWZvC|p#4$5D{A<3xnSAq|Y9,u_lJ%+RMp'~帹 <֬rv IDAT9aSbÆ(\kmi{ʮ鬙S7da;M;pw@֦6}S>_`:kt/r̟ ? W1b gJU'6E>oLzײplbVK7w|$䃮 hZa*v_;΄zr F׳ϒGˡ)3iɡF4Mu΋q߳(`_ߌere^VD+I7xvfEh?ֈb{Qj#ĶۍՊڥ2EsIRr %mU)A4"jO OR\CK`w&g$ޙds)'}X}rxz} 86-gD-~<V*{9 /J1naBF-s+ʢy_1֫7$|ʐZsDr~#եMJq&v 1 lj<=yDnh.=MbR/dfF1h[l.ߗo1{?pfbA`˙Q{=Jf%'w7߱:{%rO6gJ;7_"*Bg9N%抛v w|&8Wa5/VO)x|LϙDXmz=ԏv5JCi˔37[6gfgI9qo}˦ss!gvz+&f̟LRG{7m01Mìi4f8HOʟdFyM`TͳrIM$7/\R3X,Oi$&%0lgҋf̯lb2b>X 4{[9ÊaV<o vb}u^,q(roKC"U;+Ex<]󲓓0$$$0B\>ITL#oت7`aQF?Y;HM38Rm۰`ޱָtՐ`~c+]6[l:jX{Z-޾G|A7,vϥM% <%%dǧ5 ՇAN$LHW>aMyb49/4Ϸiҕ ݴĹ ̿&0ILr`+JQ1pH c=}b.<~ v#xCiO;sי<q{IN.œt6\R FHH1f1qN.hq\@׋w٩(wBఫ,""r-f>ĚTݐ3t=uiF,+}xbXKҔvğ;ܚ\y)oAӏ 7r@7#764250aW:_³nr|4s*nۋAMӻB6oɟk?ZyNSU_l'Օ$yRnMu5b%ŹQjR2yAX^~5}RhU,>f Dp+^"Ӊ8(u1$OgG9pƿdQH8ΑdJ ;I8pfc.2ë<EbdA EG;.q'0ku3%8.ec4f8& . >n@eLq6N8(L!1q9xA˺.yuj&!hIq؝xjfkK_yi;:lޑLV QױΩ.=Cw'+gAAN7=o9Ρ#%dc&7bn I9:pѱ3rsجD/b]x2^[pgǺxw60 Xvލzrv~%k;#X?[Sq:_ΰpM/+qdf& |7]`uv/]d19ם(veמ˦6yֱ:ڤx{Ѩ: /1Ŷ?c1<+|TK|Ifփdfi+G!}h^kןӄvy<޿ SIfXhljxV] iDVebf3ʜ|ꐾOVAHys F<*O!=b!^v$+sh+ýpv,';HO]KP$!!.ކqؕMُ{_ G;v'a9{É̶`B΁H(Vq,φyI<7^Mbͤ\Ұj {1}Ku`p@̛/6GZqrᄋ%tc6l[7^[P˂A:11i!.·ʌO26;cy4 rÂI♸3/}*6$Y(sF[6[ɷ2ϺIkM<?ءp=ڍї_ Xʶj|/7nD̬,L E˰ΕIذC9^v-j֢G3j#}:Zu8[6at }(ṇħCjt+:愓 .Tґz^l8&Y9';⾺׏xNO=zLA2rBg2`ӌ3z#k^}1f˺Z"ͫvT.Nf ,o܏|j Ofc> ^/,˾-᤺@dIG/NIlr< ՞Ռʚ98UϫO bc/1_αrx>7CcO,w~Tw}z6h`$w鈃#\}=3"w*5Enwf[I;-Pa-R~ߗsD.8u )@iLߖ@pϗx'tt ,W򩃙^Ǭ)j։.u8[^zo1i7cw/Ipqى}} ŽIڟq, ܁L_x"R(j$pPr ""rM98D 3v;fa/MZt%ew17#&N-c]~_/;9v gI:<o YKR6‰Oɮ܃Ǟz~3YG|!;'Y9cG3yDc1KIbO{:eߜ\ޱ+B1g,:Y7{wSquuV آlIC%R"BEܥwuJ$-(Jn%{QH320u]s]psw5sxx39|\sMm_˜= ,}9&|uo'g܋/ݫYJD".?.>\_ x`fdSX~:MRktmv9Iz*D6iuxf%8rsO0!o`gY}6hE J:Agfi,cr]|4s;pSKpZ+fyZMSO||0g s]%"r^~ܹK-JͻtX^.V^Q2c-RL@-\Y2&2%""lofܸ.?'>~. V)U?kNgyƵ->M`>Љ-Rj22g%Uf #٤E`9#ѣq6$]_)UʫHɢ1dB^ڎ J5mG+s>٣,""""""gqo_AՀ"""""%DFHň^Q (""""""RRDDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDB#"_8IY}C DhG^hV3R.Rbtr9AK=W2~(7b aۜ@ky\?t*9Sue nn=WΘȤw ~-w F*YˬI=m_2O+jDHa!&fCy}VoFr:ĚlZ|Ӳ-`plKO'y(Cp}Fdٓ0{9rg#a7kO8G~oN_rB"v^ZD wVB!cp[_ohKaC$_sI̟&P5r2Ͼi7ږNx'G>#՛cʝ" :+QK׭xpeh;=Un_t0rI5g;&3q -#aOd.%3T&ӇηdbJ}r5U[Ђnq瑟";˼sW5s4o!iW>x[_;;eCO Y9FϑwDQ\w:(s_8%wQ~ePzT'հ>є)#Hbi#""r ؆za~JC;\Fq7ˆ*}Slj11]Jat!i#!ްj+y~5W!3 DZ:WYQtM_<\*Aάc_˿F6{н{FwYPcf$K'v?:=x#qpߨn#4z-K%+Z"RXE NR;x&N~W&ĠlGY0u:Iӧ3cy=1_`m83j3kJaFlho 'M92۽L:3^ޗsinOfhQ+6*|<=ko77- ay1N1uWb1n/|tإ\8E`_>f7ѴNn5yZI$9V3dyy,{pe]e8hh=s%n/L%w޶Ϟ;E 3s- טL{9oŗGt+d1/@riw y$$u@6;x? @i[E;4?'cǷY\?fH*+KKS.H """bHP1f:^Mo90Fɬ7Fbf,;i11o@8ES87^uu^53/}7.b{8 b5`uVp.47}7ߚĤGpQa2nP܃s{l0&3'/~v-% J5hD|i; yyؖ? >!/yi)u8/cFou3W:TuHeێae,8?}ރ7/}$QaɎnɌӗջ,OMyV.W?d/z-Rݻp2ٸ]>_%O>OoVrT(gb+ʩ:&Łi^>蝊)P\y=^pqHo$w<<'5^<^p]Yd\7"W3$mA]MNhvpZsonisԲ稹Ϋ&>˹`撞SS0 Wk殄 p$q񝞧T?8;~k}&f|?اD%"""`HP1i87*N' ;rahz:ĈG)Δe[:ǃQ:XQ<3-3ei+P~B v^ʴnSfC2s:s{.)k3,~qэ1,~&~{7sgqd$om}xl}CBBBpH3I_%Q&<5x@^ߛbq9`8q&:\Áarg(߸3KiHة6n3۶Q+f=\baÊd9u]{Wo4w ҡ]<(^|GFCTsRtoŊ;ؚ:e ߺUF^!QpF[ZmYAhQ;;~vZF]Q ΰxZhO +.˴ގAM3oWLNд WL}zvkH3uNbu pӢ4<;<mA8-pSY}F)R˷h pǍс[f8)-tUkS4h> s`ܕ.(@|\ ``8)ɉw|uqV/;9 \‰t{lB6os #Evv<+|U}sI1Ǐm\ѽ 8p[[[M>IØdO.vfs8}#/â,xUUjb-cyi 7GSc'a^Wi/?:~F2䮑 22YhYJi>G9Y0mVfFClL"~+ӥ':Jr}0"؁ʉ/W2""\ DD"sw!IuyQ-cT6ѪZ?lsa[ĦR|yyRVeٞ3+gK[:md8@".Kpv_¡zR1m5VFʢ޾dxyrO_>}_k Ј)r_MZ6Uk_͍ K+nc.;U7UMn-hyw%kHL[e\2.2|~an וS=v Kߢ uz<ΘRx9:7 Dr϶8/]yXo^N>ĵ_dBj 2>/5o⬟2p ~edh ^qcuFV|Q~#y)Z3-G>5+oO]i_)ޏYA{z sܝPmsW;dtA>(7$2]&YL0EldzG/gILp"MF?Fpb#)Al,IPbz%E ;$^)ߦ?lh$Ouat^xg-~3+g~x91{Ҕm%~^+` 'HlґFyV鋫BVJV61WGDe[( v^z !;;m۪Y|j^iU)vKNwC#̵e[Κ;N~ΩC!ԣ [:nkC+b"A[c}qg̖N|@[Тq+W_,&ξSxT #ErOܵK3~fGNeږjh`Pc͸;0N```````PvADD/WDDD5H y -%""""""EDDDDD$J )Z 1Z 1Z 1Z 1Z 1Z 1Z 1Z 1Z 1Zs!$Ebv :Bl x\a=LW嶗^22?iBii7bOmgkC«9Ftn>c3&2}gUXkkX7W 'gZfM9h;ت/#F2il6dh >u5ٴx㦭b^Yǵ+1ksۣ(ށ 8i^ϔa 'Klm̌vPzx02dW~LEDD{\<ӈ<ţHsUcHu7G> 0ķ+0tnLew[oLӺMu?Wg[;VmF8]ݚP5*@Ʈ5|[,5؎5WyϓA]Z&ٿ_ćc5R4A;'74_]%O_~ g e;fO٘VyFLn מq6 ߜgƜZѺK`޲9f߲bN2L'ԧe賞$Moaնtfѣh>^?y/=/Cz:Ǿ ۨ^\~ϑ*7UXpyY9Wp$m!tʷMfd&݇[FÞw]^WVq=Y씩s=5fې^?̝uO;' v6n2 Up_*0(bcfvCL?ҜF\Caզ:Z~aFu cp]@߻n]9\Ih͋XmLjlΑ{E= kw"?cwa.^u s8=L 2ɡ?K ""R\J&CxudC~ţHʵa̋}`xbŸ }u9y&Gff&..iހx=Y|ּqnζW+3x WmUحk!{lyb``i \I͋XmN+P0@.N?e?"WV[|h3j#Wt^QQ6"ճfxw6lt1"S"e4Ow|ֈDK BpIK&{u<3ةѦ^~#5fv>_l3x䙅o?9.(s973٬T;G[5M\0(wE2mA݋x{J_x<Йvo+T~ 哚rrqgxvHt^R~?}$VYa*p(%͑TRh ے-YK}jJF\%l2g.WUa/tbn܇QBbn/Lk"( g/,gqenhǖw2~| lP;p—^ɮ`b o=HQYx{xr&2NG79xH&|m&N |OmſJ87^WmU8Q m2i|2m^}ƾ@(氰JBo^j;\P=in: o5ItdC)yt gXa79yκgע߾ҠTFχyuG^mbKѺK`_ AI쎰l|~ /ނ7cOzzn09} =od+ Uk|df1Lj%&MlDMxy:}vA+࿋)0zr2L2ww<<'N^<^p]Y܎0O^sYT w{W3$mA]MNhvpZ.TmwwlNUg[M稅p:G-y NW(JBm^b;p"]Ծz̬ s;B)y\s0:g]q7yɺg*&7VZz4ŁPہ,2)dZ.Uq2|t*8e'?;o3Ŀ 6{ʕa8.a;]-8ӓU . +]9\I͋XɗoC^r+n㫷gUXc_rHȫUoGgʣr׼=Ђ=5[ՎǼ8m?Fdz|.b?V%dR$ȴVB 7 <,޸:W ߘ8mf{rHF><?׿]RI%Jm'=c9M2SSɊK$Ǿ#d}zHpϢ-+'ƞh*&D61OHRQΝU0K\qM=wxMLcaKc?wz.Rl|eJ秚pȏ;'mSW1Q?pt "#A}2L XlzgU )_4v{3@ `*+ yei+0!j\!wZOiݦ6͆2et]*݃SOgX;cX#Loog[8zɷ>rq6v>T&}_̼6NَݫVqQz+3,V=Si ʷh .L?S-4SsL֤)kXǼ[:w%#׬>U# %wމ]S8 'e:9.o".w`7%pSz6]8n/ٛMFtO70{0J75^DDDDƿ۸{qǭķb|1ɞ\8q\F*}_9E[~4hB7:vꝀU-ͦPm7 wV߱JB`^G9Y0mVfFClL"~%WK9Nu|}Y%dR$@+LIFtm>ݔkܑ/˦L{Ŧtm]c>g6mǥa;r?ƥԭQʶ_z Y5hvC#ʧfʃTd:޺-M6%M[qMDY΂yJ-hyw%kHL[eA3>^|an וS=v KUif4h=RWBviJwn!5j@7yqOdjԗqɕٸ;ZY:w%#FV|Q~#yq FB_`0 lG|&"ILG/gILp"MmckALrLwzTDDէQ5iסĵ_l}v_~׈FП=0 IDATl+NgU?sxJy\kl8T bH4c#!x1lؤ#ُG1 !;;m۪Y|j^iU)vKNwC#̵e[ΒW?汝SnC3G;0u x_`I+77H :-[ЁԷ EW,ʿXLL9}""""""""""""""=H>?w錟Ѽ?oٸZDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$h!XDDDDDDDDDDDDD$8#]A lQ (""""RhHD ij@ RDDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDBEDDDDDDDDDDDDDB/qSz) AU_ඊ?rg☉bg^~vjٲA<:uI7:dLI|;-F2&l<>͢ƍ2G>}yv.ϵf潣7"njm_#  g>glXkkX7W 'gZfM9h;Ol՗}ZQ#G 4a62s㎸ҭiuڳٻ~!'aQ0([+^Kp<7pqL,=> m<'!?XH9.}ki4M[h"#)}oűjL|'AvλB_0w6y_}y%~{;p>Km$0(~DLuoyՋʭ0;WԌ9ȦoI+6E7iU(g 4L=z$ y}&]yŝ0sc*Bxc?fmzeMu6'}G8Yǟ(._9 fM]X*3^eBv^ONDRIMvp-y?}|:eߵ?o@(]1{o>Gl4~ig/0ͩ|KNH䵸KH!o A LpVK/Dy%q.gm ,fvV{f$Nc+Wodwvoy{{r~JԒgu *nzqY9NcrX/ ksNq>qS, }dj܊9tޟ_<']BTq{߁'V7o)Lx=1Z姈2jt u~~䞽zzku{NuDq˝2unࡧҬtmmG3"_(b8 Q#,{'x=ò^IL; aDDD{5ח zFgpP "ې^?̝uOW' v6n2 Up_*0(bcfvCL?ҜF\CaզZ ]oYTVm>MP[2Sr~ys/2!#y=1 HX8xX1|'Iemg=Ix]s<4=eak57*>Eg6:@Rl^ZD KuWHM#Z5kP't(W.lQUm~vlܘ}l8Lga5i٢)"i\W3F?~-_JHFlhoۍ;Ǭ3{z/uQ#7`´o<}S>䓧sJi?;f~vfMg3}yG{qgM升:Q; UڡO1?c/$_ap.,p13u҅8wQ̛6FpJ"ɱY ˛ gۄ+:-A){ة2 9oN Khmc; )Sf>YX:]HF+P0@.N?eUjRj<<25y?,M[Oܟֺ) 6NEv%w^gϝ2:mƽ7>Qc?}sƗ@yW蘟#wcWrW_]X!:!}֋a#'21zɘڅ,GHJ۽d/?ÝD'7tOZ=*""R,e0-9+qEU+230qqId/~79糵整s[eMQ 5_7TVau:BtNXs1f[o(01bsS)|nB_k|ן{ "ylVjqmݾ6elDgbl3b6ETFyE 4hJ}2_9e$mGat_VEдrmWʵ."].÷Sf~ {\RIZqi~v lM^@VٔSǛҾ~f٬:pN|RkOptZ ے̵|mʟ,jՐEoڗ([YKФ'w^A8v9}b?hk hg-ԔCm˳cߠTDGi&R V?;>Fb JI{]$8*-Q:)ꖛZ/\P=in: o5Itd J_Tx_&?}c?;էž&dU"OxPrwm yjU{ɓyg\_̠u9g?v[c>G9)w*;O3k %qƛRgR84#&Ș_Lt Ԋ$n\Y*^$!-L{H4i{Jب+kuODi!F҈7h[̌e01p:mxe+?o=IiLsאiڈOnm^Ƨćk$m_"Svzr2 +c; a"b$w*o$OqbpY?.u[1aBr$&Eݮ]I\>|yeiaurij$_NǺ :n/ޒ ..aҟ^`ZQ;2͝*6cq]ҍ߯h}=tai-Xowe]ɘNJ:2 61KvN'PyDʻ(:K, u #""R+v=\fe{FYu+<,7ݜ+yxL J+{>ٍa^.4u;p8ײ/YFMacrgxNPS >AuHDk!i%QqvǜDNC߭k yS\{Z z Edfr̷ثpk(6gYdVz13ׯ'i;MӥDz.-"VY)աSFT2wzׯe/-:0ņwz6 .°c/mzZ2<vfÆ"1Yw4eocLi?'fGE^8#5F4r`DU&/̎)p*1#]nx<M,S Iҏy l۱9'0-k؁^oʉ!|)@^<^'Ŕr88q~Ϧ,bnylޱh,]s"XVsw14 .Qq^Mlv!Y Q#7(5wEY3/saW ""Rfѵ * =Y/#{31M`V:ƗRe15^Zc%-Uˇ^oY![4xy߰T^[7rzשO.Ϋ_w]'aTn9oc~gE{_峩 ~V̥ ̯ui9!5ÞM൥кeUu]LZ;uw4d],r%^^gI! y䮻qMKK%=q8:g/V:7IuG2.}ۑ\"l iuiu oXB Ʌzѽ,B.d_iPgD"mt,7LjZ4Hr4pC3HBB)= x';7Q:=Mڨ~W>A _ex)Xkn`$(Ɏgz?F'g[1 _Om\޻ *8p%4_:A6\,`?0:.yXɜeMQ;ܺJޏUM3 W -H/'3MdAn $W ̝-3t=/tra~tq㛵Ks]ZDNHrIFEE*Ssl.4֡}m&yйCM" ö8.jښkiG}~ʫFB~z;[ѻM2V|-y*ҰAjըDoٙ҅5}-k5K߹$hR4iؖ Uߨ!M2{2\D|6x~,ku 0cu&7ګ`US*9qhޮ=7N ⧤'^vFT3|=[D<jz;/ OacæMnA=鐘q18_gչ~0H9ы%߰:"?gWNjy|YWD3zpohLZ_j^4-{11zΈM.4m!'ՇrGDK 7Oi4U)W`~gYFfoaOJgFn}:0]>b,eUSYXS\@Тj Y*`?esKarnߓkgbDsjr]&v^?_Qy7㫟1 -*OQ$;ƶp -\3z(m&qInc^sٷܒzܙy[RX^#`XN4q 4;Dzl]Z$zuy wwЪY _Q[_X1(W%-*ms*z0bػh|}O$""""""""""""$33~~٣1o3O]CiDbkmk>r#:c)VtDDDDDDDDDDDDDN0[]M?% RxХ>EDDDDDDDDDDDDNeѻO]BDDDDDDDDDDDDL)""""""""""""""E`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3jBPv+"""F@AՀ"""""0+EDDDDDDDDDDDDD$,"""""""""""""f 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f2͈J$@P˜|? 7'۟<[ `<;9J7f7!>!0_U޹ f&f -ew>#Zf_?>qbo_Zsxs2;53da IDATytjZssRp^\A\jTijk5>Ġ7pЮ\/s s?z~LxgS팽-b}W:<9t睽o .FEs L:r@tΌz=U!/6o*:g-|^LĨЀ>oIDxؼ^8ߊ#a HqmL|s:?cᰅv-oYS^oYTUͩjdU Cϓu LB1Cq70 koy׸_ Pkj$Jyjay?K"HmIևӻ` w p7ⶻ;?zt w#ҩR}^Z>;)sm3NfN;ygBkyC|gC"v &9/]lcx1[zTi[a#h!DV@z,v`Zj"""e"nFb3y3 nyyu!gcoGcu[{5lk5{gF떇x?%uX/j-˚ʺ޲9Ra pU O"O1{?<Իv'{F>ͤyrMԩLiݪ `/Mǘ8i2_^H8Qo>-^Z?3QGweo U|,g3<5w?{>b߶x'Ĥqwru;8G&O^ Sѐr|v{^ҮM L:Q mկ{2s i_i9  ثզ-SȐNrL~2eo_&t'7GqhW_e{=Y6ra$u~*ty| ~Z?~X?w# niIԿ)韯=kQzîqwqQ,2 sHr6뢪S/j+>m+/#8܅  ZNmHZ5O-Lc_֨-bc~]I> ^ۄӿas}5IT܉|&_ə W81N*h#c/ݸS9:FS%Wɹj`dn LXGy2w_nt-PhFF+x۹` fI|0/#t--S0>^׳e?gC NeMqd ~5B^{eM尬BVͲ 1g(efznÐÉ/'F9Z6so/+`Mz>tᱮ@箩ViOfHsR%`#NMbu|_2iE\\I:q.5Gy~͢/qw?Wojvݬ]CVgի]HYO|ǾXF;sؓBQrg>cJnl~nl*YHtM1/ jЖ>剋ٟx ۧ+]Ж.%HwvJfy]$vSZ`y$رP4bD܀/uO/rUAڡ2عM?XE +<55/k"їƯ_^ko+{7u8:1ƶ$Rٙv̤(=UIyk]Bȏgy+x Odȹmc oswīuIo^$o^ow{7Nlp'y53w3hw߫Lt>֒l9U޴tz3+teVs rW2槓i;n1#e{Ai@$XZA~[ ͒/$qk'F=!Pr (‡k9Rs1WB)LEx3w蹾T^6;l1_Sבs:-Ab[sm6ӆf #b*ܺ??"or1-k*er=찪ܖ5gX9C9,3~oE!5-y" m^i6/H u$ww'bsYkj!j?x"`ܣ1=΅pɡH09_fО {k|YcojE+r^FqILMKS_1w5)/-?=.dl(6=y>z(р+$ ƽ>O}Ilێz)Qh{=^pq[1EŘIU,ޜIM;Ô;HW',r:7ʵNۃOHbo^ WE4V2OC{?9ݻKb{iwD,K{y `/YdVz"vQkW׼?})upE6L ~fɷᆔZqa).dzx]DBf1?ܕtydy ӇEN\oH;1׹p8z)72 2FymM|ˆ)9b7x͍F`$-H9˳yX0n0=˻9W66*W}K]hvpZpY%kӨ)Bv] j[V5gX9C9,;~-fC /oÈ~70z^BbVkjj?x"*].u\:uiD%s!Z6"ߢ3 Slx7g?J 1 ~qHִ Fd<]$FC%kX0e5as69\d,>M0NPYK$snFy?= ssKܛN:Td;|BR%9}izrɯ\C7Wjb1;ӳ9'BN.'=ru5gngڒ}Y@zz.Ĥ0AA(W)Hf)DH۹DߨD+gB35a@ò~[6;S hX6fv8ވsxw}e1$OHQ.F̃Ii8vqľ """"et!j{},W`t9z<^OyB[ql+5-h9V֕~-oUSB,aŘߺTK1e&V&r|y`6㪦ix|<}p3u#.宷_`H}{J=Zk~d橈Jgi[׭ef v;s/RO-@q{^%_dUwGuX&Yb=]FygꅬȷQƱ<4?c5Sbhu =ş!<~Q6׹MZw$.Ekae<]#%IѻE>o%j߰FuA# ;{?YAMiU7;~~NN7ҵzD֋ۿaρÅE5شqב]1ָ9 `fdjwp7ozl X8!ukOOhUoY՜0៱s9,w<׬\;VMzÆ2\=/Φ(߈{TbsXPrQ>E ^J­.o씍uub` }Y%9nĵeY -s+ ;/hÍ5w[( ?E)|#pd{4z(m&qIn~큟kb.[RY71b@7lq8+@0kho0oI:>Mx/X3gVa޷#3dbn#c=3;y;#6TbӴl&gAEl ǾAH#feFSe>|&C?Eݰ?HOA\E+ sz@) lZyWsCXL}W'f4/YI]Gv. 9yedhtfA v)ۧ 3-!PV5E|;)GAʪ:Zj,5gXَTsXF\[$U|\;<,NA %5f'ߗQlTiލlX955˵Ұ>~0u6?KQQY|O/WIݰ{wmjl~a\ 6lDzߋ :;:?AffY 'JݏyN?{eJ5ym\YS5 ]k(X_ Hr0wKbMV%̐n5(^)|ruOr}?MBDDDDDDDDDDDD4""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3jBPv+"""F@AseEDDDDDDDDDDDD$4HQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,'A|bS?C!oF4uq̷no; ӝ t-!2/s%9n}f7XW>!7ƍM9;կf̨^IeLo.<ѻ*mogm#}Lƿ29 ˆ"H03Y7=^xo)'sgswn20d?8 ų |ϋXAH۽/ o-3A1kb PQ>Ysڭ>|fobof.OknJ,Z˶ ثѼe Qqn~Y5Kl+9Z1|?w?;e{A{ 1 q^Rv""""""""""V>G3ޫbW܄'%MSM;.<=y*_Mel~ [6]u;i ]TkԂ6^HF^`lyշlʛ0: ?ôɯtFT8t&d?_㩓Z)hHf9x>;dIciצ4>%piC"|+|̜loswZqةֺ IfikYb,lѢ]]~PgLȅZdw{4uX<-js#FB ZnR[L;& IDAT]3lDs&/9|ڹpuaq;D8nd|&u.6n@S#\ItbKƘ?3{11LW] ;ȽYeIkF~ @ c1#c=~=-\ Ar.~&_l/Govp֤߃wrC/K?϶DvjUN LOz^*VMM_~Š\rb3vv要 vrSj<;VQyMKsh3J'_ca[%δ/akRNN$:◀U+b9wV1[>q[L`ή(܅Nǎ3w80Llq4%۱ea.gUgi>|^ծえ}G9WW)/>l<,+_?f]_?[+1)qN3V> L囙c~RWc|RRs}8# Rro=$wZoLץO'auw<<E30=|?DFx=urYUЉt{.e׃e@0s 3<"涐smW߷z{'[Vpޗ6I!{MLGup<&fypnf\Ɓ@DDDDDDDDD>G7ÐgX f˾OS˒SF=~0} l6[.`U7p^e >#WO.x(>*Un M.x;[܈זO"f7*ki.Wh⺹`i `4p;3|}A< hE$o\܏/5uã(†|"""""""""rC]ի{ۤnt6VZ_p6DDDDDDDDDDDDDOuV?ÿÿ]Z 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3j5EDDDDDDDDDDDDDŒ""""""""""""""aF`0FHQ#XDDDDDDDDDDDDD$̨,"""""""""""""f 3jKx09R4ƙHP$4txyQ3[}<+'wFn}Jʷ͏zO|;#^N"-~mt_Osj䭞+?[K$I$I$I:I`!\U/oڻg}54B(ЌE\-Onw/O~O WǍFӸ7@.gp1&U]NIxV;\ȆHalzg\ve\t,kr)ꃎ-| ﶙ'~p-G=s!I$I$I+O"-7;nZlqY@'6l >5p!Ө%WԠُsA$ A/Zݷb^aeD:36Oa u8 |nqlIm%NRCdTK'-.,5EsXpqjy$Io($I$ $uH,3;#h+kV  1t oc/S Ґ56#Obꄝ6޽n/:!l=B3;[S+5(J&tH7A?#n#1o)^'j]#I$I$I$}s8H?´Wٍﰲ(Il!!'#dj=r]T,fw2/w{6AڥLy0i;29Zc@&>XJve!9p9y?r$$dӢwM2t/yyCK+I$I$IOB.GJJ"R *"Ʀ`Np}O mk3_ӋڐRc;( f=j'L|E!eDDtlr /Pm%*(, $I$I$I:i"XI?M_\IQqfNҼ^urõ{x'D 62ydj67DfYSl%zBfƜwE4\=n<^ތ-33Ԑsw7Bv_n;/eGpsWRY|}\t@~=S Xj;%_:{L$I$IWk]}nۦaY/uyYO6!*Zu=/n! {?N݇I-IxDBu\\)2 RTT:G,7کo0$گC $'){C.K[(\%%W2/3=w bEOPHH&9n6f>IՄb^悯pm^8zK^け)lYͅ[xrNbi$I$ItT O6Y=T(ՊHȲPI0NK3HӫWX)pjQ*(g# ZA\7= Rٲh~#fmˢߡćRB靹(/,*#ٕ^ÔΎ_ҟ ]tP+S ޭv1wFuHF$Ŀfm֛sLTec8V/nQk_=H\&˻wpP !'ciøN2% ,>C˟GBݻ2 rH66HTcgDP2ڟ5SLښ~ i0-˙LPJ@<;C*㡤v&o\v՗Sf)βU]p;=?%n+%_ܝGeǰ3: RbH}z6ƀ8:\z{ǦgrmHXRJɛ70+9.Lvzsht`b㝵?ᆜs$JVLx$F5/ gv`vOѼ -΃ZZ,_D&Y$T~fd4E?n zJ:]}z'" Df˸\5.#~gX#؊ӤN!#@h|[C8s{?A_I:!G>..伜M}oޗ6&Hn5z%1ß-̆,~-mcҳODZ 3{Is۲קbCR/|skH׸yfn<ҍzq q^]U3G yv(A Q'аq"@J\UGџA3 Z~,~~xum]ν'ujstG<;~eKQ+;oCф]v W7gx䐽a ?Ozq!YΧwCzu-bso3 _ڇȗ_ÇdG?s KIy:C$:sſ٭~Xrp:&m*?7!yK}c,qkK*㉴mz'//-!a-"14<3zveqqj{Adf ۓۂ4>>]l~ )>,MÔ|*sMSCyt>]ʦؽwwsf&ĕ}Ϋ/~ƎyzM_~p-e/zxм!2Oʰyѿ3sACY{a\-tlⴡ]My?>s 1< ~->~GOM>NxHp&w,'ʝˆ Ӗ#jJ8{kh[0Ggtnp~!&q 2ʂftK_yx. ;nvo$1G^ Yъ5wW ْv50wihPL`%ϑjn~{Q MV=jy<~fJ$I$If|2 dG-\ߦq-Yse/I.cc>ES26FfVꁁLR q33zk%Džө}2O`i^Ld>hI̙ D|>%#aұu?ʪ(LyrOh۸6cךL #t c6Fqy!,c[z&dZo3cS ъVNZѱQVk؈Z9k~b*rP=HfuذtG۟q:%мc+*cҺ"BVNmX[>Dv6^;1 S+gq|4%K2mk2|?e/k8dtO9cHRR +IM ++"; 'Owd<|gxsּ 6N=fNYՓ' %ꇏNJxB}ڵ`Yl,Ru>cgl<쳁#>e)}܊[В9[簼05AA5EKjbdZĠe9?]ť, O]A<)ɔRG1(fٜF!(]M;,|"HLM#TR]9(*e]QYckPNIYVtIYۆl9y;ʉod'{OӤJ845[өz&]p IDAT0\ZthJRӸ]3RiUblO2fJb\2Iѩu&! cgؕGA2LM պ5V.biIeJɤVdޚG=Ẵo SXY%VO^+h~ _P~>cr2(x_J B2{v %d#ƏO7\#7'Z]x;}L5'&NP%;X_Dj ._cy䅒IMe~qPsS|10P i!wX>4K'p9+NqV-Rr YRs 26`K;<:lHOZ~'e~ZeKB%S_B )"Q\\NjZ*5FE岣0+ drf@yS)[CP#ۋ;C҉nYʤ11~U)zLVnм].StQ½?泫0)cJxZ El] )咷_J\Oyr*{}b:d%mZz*k4]#PXj-2+-)ܾ-DBe#KU"A,p' jr( wڶRJ+X,D8t v2׈;;}dx|糵KñN\^_A E%IOK%=-LA~s]~yAҗ T2֒RHNZ|νǜÄ ضpUkNт S'=H=mw_^LqtJ9'ѴM6˧<|BI9ۜ {aG'+iOb6!T]6Ƃ`$I$IA$DjP#ԬIl璯lw()e7joDRۮ?3SbZ^-Baq.^zwE|\*E(Jݿ5WDO,;| )H'+#{gdSPv-iVFLXq(4ނs.?9=JMAq2iaؼĉ pzvvU6;LI$I$I:dƼ6w?.T<}Z4})_C90pR%Gw\:- 4Gefez)T r2:k$‘tZ@"Hf ώ*28N߃F"u>T2W.dyoׁY25ӥNH*MzSh)DRV.DN.d_æhWѢk;*/gG{eѵ% %P󔳸ạ;LJˢfj<Ү: 8K,Xg;ӘHY-ҧVf/v 6J橽^'H|5M-!VI $$$(, TApf}⢾̈+) hq+cAlޞXHǡi_a:׊"dwJE#XTQe %B|vktgG7.f~q kJV\rZ]6.Xol+s|Π6Ո𴞴߻:D$F\˕=n]Ȣyۏ#v-Yz=ҡsaPb޲T:DE8{"s94]Lidfwj#݃ś+iOl# ӶWw k`oypF}ڶC $I$IpEI% 7Hj/ʡ|:m!;^ʲi;+Elt^)Pk3_G{\9ޠ 'q䮝?`T`ξNsL^~cyI,^Tï#͇xJX2e>2nCl|ύO^ǡ]\o|`κNk"ƌ%{\Ɣ@;V0e{ԣOC-i8qfj|v곀Uwn:' 8?ߎl>{e>ZZ@f{?A\Z:S-TPι6$cg T^3Y\S)/)fNJ؝I~ĈH9۳@}Wgr9B9X٦]:JnMM h%8uрcS;8ƣl;gɈ!p ӗ%zZ_ÐsO ogWyavb0Jwi zv t{=/`9~pGMc|ZD+ .r#RJqF.XJalE rl;sn*fy\ב8Z?I͘!CޤQҴmƎ+*I$I$I_۷Zׯ`/~ayye PhLZb ;a㰫Y!nj'S?~ߒp2YY4՞oX$I$IR'X!GO߳/$I_ jsg˜$I$I$} %ؖI\ߪc?݉ )|v>! =7Cگ9:OK53)o܇.F*GOtz hƽo%4 %7fA^wUf~8w$cw֧epz- ,I$I$I [λ#a򌏙b+y78u,WJA BF|YF}}%}8 LCY: pj6L8Z @e񷧟Iٍl7FsGy Sc'0Lx*JD?X(KS0 ,ɬQ5LLi먈#J&}'*'{[ǭ$I$I$IFrkhh / RQ Pp$0Hn WPvEOHUCuz3S&zF i%:C;’e{Mۈ@fF2Z6o]`&6W8'.I,?X.v{ygVOKg׮u+-4&>˃m3_o.hه (ypߝ`v`I$I$Ic,bA''7?FPQHYje[YD=#8Bi"߄CZu ?L'nF 5ɩʉm@Nh H$)!L?̶tKpߏ9O,9)#P DbJ[HhԖ΃.CL}uF$I$I$ X:\Kaǎ3be3+(9uHh M]{bĎrvA^}*V7`483s6P4 1lB4܄#.bz߂ppi Lͮ$qw_Ք^) ͛ $I$I$gKUL'x÷iU80cwu) ocڮ,]l Ўr{z{j2dPyO!bX7)FL)qit:c{ OSxwf9]׹yo^6ùcDb&?Rj`I$I$Izvo׹իg{0d<5Yd!KJoS>KH$I$I$Iw|WTPş/~ayy_yZ>uzq{nϿpWΒ(w$I$I$INțiUO#Zo=mreI$I$IA}[1iB$I$I$d $I$I$I$j1$I$I$I* X$I$I$I`I$I$I$Ib %I$I$I$1$I$I$I* X$I$I$I8#b'H$IDI$IkK$I$I$IWD$I$I$I`I$I$I$Ib %I$I$I$1$I$I$I* X$I$I$I`I$I$I$Ib %I$I$I$1$I$I$I* X$I$I$I`I$I$I$Ib %I$I$I$ *L%w3yD2~~;Wb#5r,ЌX>sަͣQg Ug/Ē6M[%ɮHb/b)DUN͇nä DY,v(&2=By*$I$I$1QUWo%8%nSٵq?#0&5o&Sf/>o,)"\ˏCr6yG|ٹqp6ݾs+7mC`;>_KU} Zv \3 Kؼp<<6UKiĦ }Zb@72:z'Jjak"ɩ:rA%vvW']}NdT9lj/ m]OJB'ԕG~Ls/"9X{Ŕ~{#O-EdMa\R7+kzRRQ^Aܮ\qt(MD= P!_1驧虿}( w+^ \,I$I$I*/u?{וV>CY9'lX:$ _ǰ<"~=7eCX1z9'< mX@ɩ( ;qTa IDAT\{/뱑sK/(r$I$I$IUN]RPZBi\uABbN C;L !JK)-?kҀnJ 1qy2JIﹺKK A<~ig_Off΋^OdgƥoϰtϭM*aSdjN߳ms%tJDqeY[F6f5'A hs5EykIuHCvMȅlEgI^;Ԏ4a}e$I$I$Ib\Tl6P{Z`=37`׆ ԮGFjSuv{vGYn+5aMJ8oZ3vZO抃;.ng6""V+mMY$ǀo1]A(>Gި$I$I$1& tՕQ7m@ W'w1¡:TULxA2 x}>fVHmq}o=Pa&/<.)&8wUU aHB2 ^\4 X~"Tu z";?K#8ZG$I$I$t 3VIjg)7_a-45$I$I$I:\KKL#!I$I$ItX8$I$I$I^ %I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii X$I$I$IҌA$I$I$I$`I$I$I$IJ3ᣁ I$)-EH9$I$FI$I$I$I­%I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii X$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)K$I$I$IRpt»?2.7׊i6ڎͣư$>#WiQu +Y}bO5d/E)OѮ<@vnjbCz7yMr@F>z33k4쮯~%=nN塏$I$I$IJ+i/JA#wI-Ȧb5o<5֓hp8|?_Ofʬ $ڬncgHl?]Č)Syie5A /KѼ^a/m#҆^<1mXO')r8۷3n2㧭!yc,r;<3a^Z&]( BTܓ+55aѫ?mGnニ>eCKMuG:s<,Ц ʟ*aODM[Qo˪[' z C}Jz\{ ] ~%I$I$IRsk4cӴy>zFm4c89&ѓ>w&|q>\ry4}'u8. ev>{KČWue hwX”rui1?4BGmgw!ߵ~97K^#qie5܇݆QV/ 9x(Fefcװ,w*Oc]S캇Syc}}Y|z$\AvGKޑd/%2_;Gf>_"B|zM}ʹD;>";ٶlî6svX$I$I$?4*Fa2jRI6{fiKnY8q`%f.jl[<4y3<-f~ U*V2AxT c<̋7MÆS=!I$I$IҏApKxϞl,h{,rMUlSiߑFo{(+%)'-{&ǩ i^=ذ1NaQ-jYRy2D(,jOyIޭíwCm7) %{BސR*; +Yabk j"1s n3{Q6 -. g"y:>N畟p}`[/(!BHW(Z:v8xD&?{.;D7Gp%O[,v9e}WwrJ_uW Zv/_ΰޣl_$I$I$) ZFxTӴ"^ (M& b 2xV {s$$1P}{,XYAYAw$;p';Aa2A2#a 8 y7_ݓ>ڦԑr 6Ԡalm=uC%-l 6Υ>&z %o:6LMHCrޟIkȈe"AFmz#;՛sV>qwaja <^LDzb_(fђ$I$I$)d8Nb.˨j+LԒ#+x  ve1b1H$H}#v!@#d[mSO$$\ #3Q(D>GÐKX>v\Xلacm' "˪`j)MmS_}(Y c$[n>6lcKʋh>Rc=#d[X3l'r=W?Ƞ_-&"6K60!I$I$IҎ+^@GrϤ7^ʎ&\P^J)=7ŽmR>Wl/-C!{.v( [e XSl(m|) VM_= e|Z֜WdzS ]Lg|'?oo!ՔSݏ_ͤv 砱gZZn*|`> @)93n+qlkxGJJdΛj<ўg2o%ϒޣO ZQv<ĀG փXkX׻9!QOgݠj.ZK$I$Itc'3'm<-ѶmZfE ȡ1[uCX7F@LZ> W1wZR@mO~_>puZwed997 Zum2FmEfV>#NSVU<3vB\r%˓~ż9ͣm^+rM3gK<:+;-=S N s&3R[XIpHmH֬9zq''z~/LZDx$Naÿbִvj@l8>acc{ïS@jgӏ1o|_=sz,M$I$I$Ii'zNqDIwn!f,yh˨I?ė7ö.Zeќٿ/y`z@x"3_6wr{|q=rHϯ!@e;f ~wΡO]<#Z4PWx`Ӽ=%#ڎ.WܛH%qytYiSb۝\3m s~ 蝱*8V(k&Q ճ$I$I$)TTl_uνwΙ_x3Ͻe.f,i?κ{ɀ{fӟewqN#I$I$IN:N}5l‡/fK$I$I$. %ݑ$I$I$I:,EI$I$I$IJ/$I$I$If %I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii X$I$I$IҌA$I$I$I$ @A$I"I$I #È$I$I$ICВ$I$I$If %I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii X$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)d8RPuMkE}4mQcXyRw ૴(r Fmǔit(K˚kʹGf  7Xvl)$cOJ vW,d*ۄWA~S>J$I$I$ctڷ7r=ZEY!{az Mԃy.wOtǼL\?v M1e*/& +X2 ~B65팩ikHh84 \L}V$lʩvlx'aSqOzX[Ԝ2eG؟Һ߻.-%7_x=uñA6%WOf9G_Kٶ9o1wgņ Ďd g>}+6{3 }Fǰatv<ۋ\DZ+Rl,167$I$I$I:5t~ ?3;s/ /ehNu hƐqjsLu '}@L`_ p lU6+'샔ϝO+;޽81i|zڝ<0e\pc| 7|3Mѷp]lw-_M4ҭWqq5%#nqYM;aa+HJeQYdzY5lz"KʬӘyTޘrox^59>xQ>;-w YF s NG9ϗ3|:}~VS,;});nEϾKV=[+of zcSQF4l st^՝峓N"9o:_YVI3Z?q3K$I$IÓApK}7\qW dEavv4.dālMmL_Мw'O'f@d5T%X f&qQ, 0/\L6Q:N{/J%̞*%<8H=|Bo>,*OP]|a]OLAq3"wQuN5o͘OvR__ɲdIN7zkʏ[ۋPf^G(yʫ||mE|oH!민;(z=4B"|rr 2hta=+|~G: J}~?#ə:RH= *)#((&Pױ YVSNA.;%?/<2bs/I$I$ItX2Nwat==ʃc{߱,DXt 6~Ӟ)-)MQ>qn 55>NXH4UƆq hWbͫg$BaQ{KJIwCm!اtʯbCɞ7Eం?l<&ʝ^oؽv@ckV74ݼglhZ]@ΖE/e#ٻuj'͉}+?uN%_PBկP uDlkpO!N*hSc•?mվ~(]Y7QRGY7$ 6o '(dg2x6S۹J֓۰̂NTW.۔G.Nd#Aܒ*Z6o  >$I$I$I 846G(;әdf$& &K)%fPر-[J6!KKPHK(ʯeCV*jٶ#EɆrvgϩA beߔ:@PƧeX;P.(JKrG}~F,?#9 );`.jȕr~&/O6yȉ6ͬ;-uFڿ2Cْ{S N s&3R[XIpHmH֬9zq''z~/LZ[;ȟ_t:>[[vDIDATiS;i$:ˢG;Π]#2f> .@`.c~[0cWpm,I$I$IC0t3w2/37Lq1n7Pg秋yᑇ͒Ѯ\2 e_F͙;۹9Of' ċ'2eSx> 7ݳ)d<BTcp[qj8E~&={C8P=GC]87yJ>67cpŶ;fRA>\;c3uSxj[: *Gw2dZ<1W$I$I$ڹ9 yE %'BY~7p,s4<2ߩux$I$I$IRI 8i~~YO9]C`I$I$IÅAtx;$I$I$IC I$I$I$I X$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)K$I$I$IR1$I$I$I4>:$IR I$IߑAa$I$I$It(Z$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii X$I$I$IҌA$I$I$I$ @JW.#rxzof"mcsjچ,xb!Ro.};c&3~RA+7+N;.B*׽3S楕;iҸ1څ M=bYlPscJv~6>S>X{4>n/sǮHdB2iKkٴA񜾬u"KN[͠K "9gǵW$I$I$9Wqax&CF_ǩ1a%3G|A$%7Gw2[Wq۬lΟ8Rhӹμw8λG̨7pZ_v'%L)\ C/x-vv2]s) tU=bwoɈFq\V}mek RkTl";ٶlî6svX$I$I$?ip\}7664mY8q`%f.jl[<4y3<-f~ U*V2AxT c<̋7MÆS+LS{m2]M8fvaUy(چserrzS !$c+-MZATh"M= "Ip#zL㧭sؠu26+_y3J{R,ױ|:foFC-݋?cŗ3F>:6ۗ$I$I$IJ;.Z|O M|"^ Ѩd" 8*V`Z*Cg! 7WMHB<#e!׾ǂM|J7LHȊ`ߎOR 0  j<={X\t:rZ"fvs4nŚĆٹdVU؆ xH34 ,R$Ȩ&&h>%ڗ%!6/ϦYc֬Qr`9?;6ϧ=SA9Ѱ JŀY$I$I$IJ7AzOo3mm6Mރ0QK"#X1H$?ؕAƈ Q QD"$AxfmO=aD2xsuXDmcaȥ^,r;.l°1ζeOjwRY˦6)/X%qd\͇tFmlIyQ_>@Rw'wo'́=`O"ӓoͫdЯot m{P_E%0U!I$I$IҎ+Y'ԛ˳~9[s_ujo(/|AaǶl)Hr+CRա=F;P_ˆҭTղm)J6Ӷc>{N P+cد2>-kNǂ+a i҃g|'?oo!Քݏ_ͤv 砱gZZn*|`> @)93n+qlkxGJJdΛj<ўg2o%:Z͚'ˆلіl=DVG!(zJKb#AA5ݜ(ާnP5mc%I$I$IR1Ng<5\>z{|xl;sJ) AD߂?5<1:eeҢ\8sג"m{2h@yӺ,#EeЪnK̟ϖ0o+2)q*E坪4箢0}X^FهsRv2Ր+L^+m!mmZmi?v8[p_Oءl=HZG~OpO3JClCf=ы;އ=F6VNӫ{dR:D۔ )Jo¬e_YQNv̆6O>Ƭi~ïS@jgӏ1o|_=sz,M$I$I$Ii'zNq71~=Drɤc˛a[-2rh߁uy<0s=I ^<W|Ə/»=ɸN9$KWr 3Z?Έ;PC'].-(+<0i<rɽɋTGi ;+5֐ 9g2Mm੉xlu0|p2f2Uɐik]=+I$I$INEUWy<܋{_a[xכ g9YvX:<$I$I$)cJWf?KA,|Bki!$I$I$I X_gx< I$I$IR!$I$I$Ib,I$I$I$Ii X$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)K$I$I$Ia" Cᣁ I$)-EH9$I$u|Jׯt?È0"I$I$I$k{?fЩS!ח %I$I$I$0JX.:u,I$I$I$Ii X$I$I$IҌA$I$I$I$`I$I$I$IJ3$I$I$If %I$I$I$)K$I$I$IR1$I$I$I4c,I$I$I$Ii&!U fl4^+ꛦh;6ʓl~G_Euwk"{ Eemz(6mO*YA7C$7 dg71H?_ISy$$I$I$Ij*i/B u|S0mDwmIz4cn7~=)6k׎=!t3L奕Մ).,/-G:J?x)|+Ãеe̩}hVd≅ljK+wҤb e;A{cŲ& 1,;z5ֵ(m|pѧ }h)ѿ0;NcE'StR2Z6m}GEDŽQ0ztFZpfɨo]m9(>H~O+;޽81i|zڝ<0e\pc| 7|3Mѷp]lw-_M4ҭWqq5%#nqYM;aa+HJeQYdzY5lz"KʬӘyTޘrox^59>xQ>;-w YF s NG9ϡ">V@_L wg[d'B"ۖTuFvN2w$I$I$I:LVCE莟pċZa@MdālMmL_Мw'O'f@d5T%X f&qQ, 0/\L6Q:N{/J%̞*%<8H=|Bo>,*OP]|a]OLAq3"wQuN5o͘OvR__ɲdIN7zkʏ[ۋPf^G(yʫ||mE|oH!민;(z=4Bd|rr 2hta=+|6[3gORv'_o˯Wl>.?!Jo7=?a7S|~K$I$I$ } {3~s8 ձi>Ҿ#ߴPVJKiSO[wMzS-:Ҽl=%{.Mac¢~K$I$I$& \'2 elشO婏:5,u 2d" 8*V`Z*Cg! 7WMHB<#e!׾ǂM|J7LHȊ`ߎOR 0  j<bqm{alj J]Fj06ޞkEfRTYgķVZ`&&b!9Ϥ5dIJH NH9uy+G_主@ ;<^LDzb_(3[$I$I$/^H}ngcY--[ רZyαx  ve1b1H$H}#v!@#d[mSO$$\ #3Q(D>GÐKX>v\XIʼnq0DF.Uʪ_68L|Bߠ(Y c$[n>6lcKʋh>Rc=ۛ`rz-׳y bm*hd#N#zho%I$I$I\Z_#wN=3 ?41 奔Oq3(ؖ-%IseR:hkPʿZm5EɆrvgϩA beߔ:@PƧeX{%l@vA!7PzЁ3Ͽ̟䷷jiƯf~UQ~F^E[sSyrSiRݾNɑpX`[;RrP"sޤUAd=y{d_@)-yװwsB${κAմ]P-I$I$It0Nk!\# Ɋr^yy& oAׅycqy2i .\ܹkI=n~<iiMS"\Zw2hmC{%JOgˀsѷY8NUxzGXsW >F,?#9 );`rjȕr~&/O6yȉ6ͬ;-uFڿ2Cْ{MS N s&3R[XIpHmH֬9zq''z~/LZyl̚-Y=Yu߯ ȀVp|2v]71ïS@jgӏ1o|_=sz,M~~K$I$I݄hYq- B# H#BDhF(E0\(:EBB⢅ ~F")AɔL1 ߛ>,I^zys֖F:53P}/ĔmYvtS1{i]:e9 I7=w{0C727ӻX0#=9|n&g,h0[Ϛ3ylze=3G2`.ܞHG1t}.=/N{ۭ\:?;8&ms`zW1?nߕw>>ɗWZxi3#ҭrڿ+TѱϭGeq"gt2yog<@9CpӶgCyn|ou2q/N͒!U?­gI6#E(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(F(xW3IENDB`mpire-2.10.2/docs/usage/mpire_dashboard_keyboard_interrupt.png000066400000000000000000002046231461637447300246170ustar00rootroot00000000000000PNG  IHDR쇤 zTXtRaw profile type exifx͘[r8DY_ 1;ρ\ dI@$t8 'ZCZT'=~;|??oby2cz_ xL׍ ܿ(G5&*FzM0>uz |_?oڿ m=%SR|R?(B4Si<\s$<ů ߣy~@+ >\6I0<)e׷E~9{wՍHi{-')/ϰG+zz䳒 ,7մH7"ĚOV9/khJ Tnbef x Wg,y=[x2'&K/'K>%Ofqeo p$wɓǵՅ`Y!>k^_ svsE0t=q/?wnh2OgCfj#p55uq ü-  X4[L՜ZF~zQϺ:JӹZ 1aN.+ H+>ԴaRVc{DF]o򨄢HLLYwa! G!C݈~lՕ.*N7v!`}OH*тunw@n849JqJ|Z?IeSy1P㜰ՎHiWj.7z1_d+/5 rf] c4oZ:d*ﲫsMe?cCynqQg<3(BU/*]ʒmڧ#qAqt:6RG gg8\ xkVF N6ǯ,\Z d(**2=2)c"o1oU+yIRH˯gibua+P6lzQ(T] Ykp{YcԷ\Z qOW`zE܃gT,B>U ΋u *!Nw*N<, n&.g^Bp[\g|7n0pnu6+bE*O"U(d2==f at>4+R4խ.Zbdc}M" \][O绪hd&מ{"l̕uʷ(akY@akF=5_B.X0"EqGwwb(ݵvd=vlHED `㧤@( s^[CPx`yAdGCX7i}%u! ֢=c_=O$K+ȋP~/!~=`;R-Mo??v e/&{KHK#?ƲD iTXtXML:com.adobe.xmp cKiCCPICC profile(}=H@_S" vqP,_8jP! :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽjiV(鶙tfE F2YIJwQܟ#f-Df6:Ԧmp'tAG+qλ,̰J|+M F%c@| ]VqcǩgJoKU`JC=uCSɐMٕ4\x?o}@ת[}@JpZkVr?עbKGDC pHYs  tIME]t+ IDATxw|gf7Jw!w(l`A׮WQ,W7+Dz $ ! GM E g9;Ϟsm;\Lv8080808080808080808080808080808080808080808080808080808080808080808080808] -*ժ'0m˓{*!.Yhh9ը][ pv!??ը][&\*ժ9>1$CTvů*0K{9mdNjf`p`p`p`p`pU~~^}wS`|ÿSmܸ5o~Qm[^3J9ܡ:x׫__O>35u}} m,KJMIQ$jիqW6Y%ypedd8Ȳ,53>:b4>JMK]{t\ TrbKЎWGGW6oT[6*/7z235ˑ̸beFF"#hpA^tcߦ`T~}}hg|m*-^H 4}PF͚93 p_V-~-_k/ۏ5w_3[pʳ.g)lfʆ4le߲|,K'wys憭ymرcӯϛ;+<}SkV֦\4J9-7EZxԠAu^oY֯[Mg5kՔuk4,Ĩe*YO$ |N<*Vvm)Of4mҥK[Çrii *$)o_Y!ikZ9}| sm9ӛ9*<5 6O m[2d)ol٦LӖװY`P pr;ic}pxFWFF$apKlޤө% TY;w$߯c߾>ݹS_|t{MرcO~WX!>% mظ!|=>y򤊗9kX8E%IC{*R{jܖ-Uc5TI%e_VmNU|||q/m[P^I#O#2%yRټ99Kay_Km8}Nv?CY ˞HF]cs<}=!8uk;RGjLծ[[+f(u\z`ȃzcpIO[샘( 0P 4Ѝ={!jgT JMKUM(Bk֜@@EutJay/NKO$}g:q9w'O/l%UrΙ5N@7kqcJjܲ,Y%۲d, ]m冯Y[݇qnFm5tN]Wؖ9a熿MPۅTuڶd[yjjvwa:icROj;-ZvGDHΤkㆍk9ޣoXvrvm߹}wi5 V{jW.Ӭ+aXG%&&jWl9ފ? z+_NcF.t'Nhws ҞݻVI7h_k˖׿J*Uƞz'r 5nD+V$mN5jTr ^<LӕUײSp+YOe7տg7o;g5!ðVNݜں9us.lrk[y<7olgp\azedd_~~j-Zɫ^ӧIӦ*WoiIl٢1G7eV(&f&No{vtiEZU`mӧi=T:x>>|7nOPƞ:|Bsլ3.y;<=4jH}0O Ej?'PNy&}3fΤQX:|P׶rd<nn6{V⚧ef t s',7/ƹaa7-w<߯orgy<y<9E[Nܨ(_ܠAW˶ z s-_Lڵ ::YMT>#u˺])r<.y*U*""BZQLLb_#G}?F-6i֭:qΜPPpʖ-Q̞ӦJ<,\KFN `#7J*:+?ysq8!p-kOvǖ7:Q,`I Jx*HP4|a&+^\rt݃;-ۭ?gl<8nӶ/>VȒe5K6 @]vѣG?^ߡCjԸ6m.ݺxSZjmv}ez;ͷ>Ygvn!3/˖{-y|MAg ,Iô틏'3S~~fNe:ՑԶ];߰xݳ3N5PpaU-*t?4;v'۲,[V֮iN8/)hg̎"߻W5jזa+߻_UөAttTǫz­֦M ۯBJd@pTwTffU4e۶233{N%$h޶ W?]@ A A A A A A A A A A A A A:iFq IDAT A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A WdP^%W Wف 4 A A A A A A A A A A A A A A A >fqU(*=A+TR?rŖȩQJ>:((^9OS*XSU~"]mWTlV8!p;EKèiբ9Z8k`kέfU9O.Ъ347X0Yߐ˶/;*ȔdFjوN EYUE-EӾ^Arifk=T/?njӂ|$M9 (}2PoCc<ZlKTo.PR8Q`5gh-ԸYl!YWF:c&O)Sޮ>kwdUKv,-2BՠU xU~W3ڻrƛ ,YUMZݯUWeɺ_W]aչ^qp.Zm jU5N3uctjjwC~zIǦ;Vit"Gܯ^EwhUJ.RSm-A/jʱjЮ*|e#+3Sqg~S'zUߠ 7/{ӧڒYzg]C>糡;N.}%۫!2e(IO2S3PgzzGBO?E˲C--ɼj_U&!4ooIFANu{RG.W=tTEd:ycg:(97*^=mj_2OhߺY1Z@U?#+)0Mzz5lRWSr><{琇4K*P\B}(MM-]Z=^Ri; azʚzn=[—^^}g*kմMбB~÷衮U!5~R轣;vFH 1+zjP>@ӌ1ȃʐ[-'=QqHӮ_wfió[>y_RyN3鮪^e>O>Rp5TE{]W2YMPۿk(u?g)2.]y/0J=#4sUj5bbe*UaI{PpjjQD?0滆yrBjuKFƺg= (ӇXGO%qmք?XU}^y*YGʨmu]2+sި4oӥjp׶x@\*}_}eDgYzau+/(y=q[8Mj\eհquN'ȓ5*u&Μ廊蟷FȽm~Qܤ?eMɬ;kۤ1q}H?^TckDM)˴L?1Hmb?jLCF~\*u4#'qn>]_޼ų!RKmj(eծc9m>C#7H襾trhTl~Oޡf[4eJR,KǢ[jQ+Vю;zu~ԫ {MRvA|XRY W'4*]Cco^U0w>zPnK2+^NȬ;_#uOzu6+Dq!ݐXH+JS^ȹ=3@B.ˬ=*o$ ZPzWj{f,ٯ %nXYv(|Vb򖬭I]ʔ2n C%j6U:)Z1m~F z,˯M I.n,]Wm¨)z7縇3|!: tL٤S6(Sd]J]/y)YFv,%O:b-:n-htfzY:nO?^p-S VDӚ[(M~[Cޡ"O2Tqs'Fij䶣J%yyNX2UW'}+V9LHϨXƪyW/Үh_S[~/K*(דտSOd[GwCj^:&jњƣ"EޖT\ J|UNQ'I-%ZQΎN=sF?V{J5N/tƟ}sE;Q WH.M-_\ܭ6MBpk2 { ng}4qxE eUrڪw7yd.zWfx`>{[v=z^;CGVK Z;gk]dvbʸut*L*Cٵ~ _x@PȵZpZ5jg#χePk>ЀҼVS=T9qfv0NQ lkWnn[t˭ ^6Ttn$>QÿdvU˫R[UZv+աVҋ {YZDDcL{ gXkկN7-:0nV8S~ ZϚtlQZ0J'rX4˜&Ә_W~]S:3WuXU ~Bwm):|uux^zH7VƱ _hTLv˖#ԡIq_,ۻ]?j<}OmJv-^S?S+3:utϋԚL_)c'Q\AcWYwg7Ie=:fPe)֩GСCdTeJ;Vևҡ%e*X]_vV`rW*FF!z4.d{}Lw~;Ň0U,ؔm*%-OEe$#}᫻|KY1"d3TCMYGvkO#;V2T:g6I%%g?2%LjO:JrrkZ]ozN(eA%#rպ[λ+iUP2$>䓶d{%;YI'<6\2Md tIǞ-+9Yɖ$ؒaTBJ1uc_ -bsTv#ua=~8`>qvaҍGm$s/ZIy7ɔWG˚qD S9M@pIْ3gǛ95p{cNS|v)\r_NP VJ\|53:r8QjؤJ 8Nc_ᩡhAwY4<FV7mgf*#;MUP5jPUƴH<'t2T>,L!Z%A^|=:h-OJeh{O R }]Gn=t]omM>S~N̿Vk,gȳ}6Z Ȑd(̌F1u}ljvH?QkRI+זm۲%efVY2PbbmjFq:fI%%ɶdYvÙ2z^{ze6LkO3Wg)W*处=yu9Yu΁bL_/سi㩲\U[ϐ+#+Sux{)E3=חR:.Leˇe쬣0$5CG;?Ud\ zz#>UWl^y\UTjry*xͫ=_Ҏ&wF߾2TDPo=&}6UGu[T-%3*Bƺqz_y̟y/Kt ٻ𨪄;F^DAiU.] bCV,EdumXWŶEDEB 쮯q~'ɜsw̽9Gv%|VFl1Z&d#79>n9|/oȰqyo,v>]oc\3j8/~(!|[ڥoVR@}]cpTOy3H鏨k!1v;v1 Pǝ&[[ӛh=rU9zצZֺ)-9숝IwWJEgMzޙ` ꕹ}_TDZ;'&3x]/zO,s>$I$ikPS7mZ'pW7rȠ)$IZIvw[Nmj3]CC1Lc+I$IR\ᗾ'Q-e7?RlH$mբ[b7H]g+q*.tݷ!y<~(n$I$̞ʬV=ؓsߒ筻$Ij.-I$I$I$I "bH$I$I$IRb0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%속K$I$I$Ijrku,I$I$I$I $I$I$I$$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$ID] գuNS~!,)C$*~ Iڪ0ዉ<ؓL6$I3%WznRi$IR-JQqv$I$me %WNSW$IRSS93i=m $IJ`+?b'H$IԞNi?v$I%I+55N*eQE$IR$5J^qMf+2;Їg1}>Йݸ[ 85kOSZqo;yꆓإ^# ySwѾ4Kx+w3Cǣ㎗rpZQC5֟DoWx3ؽAd3T4F7ULÑ<ی6O^5# )]Chԭq0 Z\N@ IDAT`$I5ߛNdư㽱0 :Myn켿:J # (М8F K>VfCx7d NI9?}ǍạpRǢ4(#! >7'A6gt>{Rrөu\6&ӯ?=!h}O>t1؇:^OgƾAuؽ i2O8 gΗ2覛q!zcKoF_z{p]n 쫠.;7e҈s988⯗&]tpعH>_Dڴ u_x ڋn-&ioexNq$AP&)**!4`2Mg݁bCGG {i&շe{O 3?WN u$Tv=z<=y%sxטہ읕Tc&Rm{I:/znUlt9Y %(yIVM.)'$G9 Ozח$Ij>?>NPgWk9zb,JX6Y8@&I9_=7q_SR@(@x]L_7,ǟ͟WS^o_z۳Cɝ{3C|'KV1yd_9),})x>M|1sء}^ۢ:66OP/d+q/3#jw+y:NY?p?-v?zW=~cf/Y;ïcRrjuHjώ t9gC{shˀMl$IG‰18{y{VIm[HYA!%]ky8̌$/4\X-Ȏ0͙<|\p 7=1H 6_ɬ9k>̛NJfi^2 ~=d#: dѺ~j#lӔųR\YT|\5MwRpZʊ;RV]iAdl֜LR6/6 ;O'41yҺ}brm$IB{xk~Hִg5̝F[rJ}A7X5 U/@xSc?;%v_Ęլ] ~bꢐz[5s֞/`TZnHȭ"̾.fEuuhάi?>^z9T/\̊ ohضx6/'G.)F% ;YG<<}v%)aSS_U/#Ҫ#3ȝ ^z5w[ڳ$)NoBsR@dr$=NvaǷ$ ɧHh@j9퐶DH& %%ƼFq_ccpޡ Y4J).갰Ҕ #)B?Jҗ_pkW沴tC[5QcPRTTXJqi*i5< `ֈhԯfFAq _@(!ۗB7v͙u~셳ٻylZ:-ڳ$)!8U)N<R%u ~cƾŘ;{2o(-,0ҎS<ظ1iY)˿ޢARJqi i)UgAJ*%El{ey[vͰ:jz2KBRRS!KkxO`RDZK.P\ik_)V㗬wS_Wa[ipѾD'I 72,)$zŷܴT[XgT4#qU>/hjHyrnr2~9qBJKJ P\8a y%'He&ƴc|pNjRV58:vV{un$4Rw?? /p9t+8_FO/>ߢ`97ysg|SA}Ē?̧aDw¿Ͽ1yRRS${'&)#Ut(ϜE~Јf9Qa,FYe~$GH@F˖4Xs׻G'j-۲k˩3{1MZ -i22g=X چ;b[ZDHy20x$G!VDܢW7#㜞?>i̦`a9ԙOjAR^̓$I}72h.sٓ6M"07$ѲUḲ$|˩95ڪ5 #tJ8ãq%WNۏ/yaZڥeUVr ڌ6͋r,nr=wƳGѫדb.qJUKh1!<^{oƦ@H,15j86.;v2Vx?Sl!tb9ǎ"ٴhʢ@$A8t^x EӔd;K_*ҁG>S_B's~֏O㳯Sy tD4s[>0LIK?v*N~MSIig#3ς>Ay~?oikcu gMJZ z} 'g5ɥDa٣iLNN#jÂ%2~ ?5|wgҀhv"i,7 7\o}Ղ{ۤGHmgݒMde{ѽs-(wgIg_T"m9ⴞd|ߔm{D Y,x,ؒ:I?^N$I#Ӄoޕw]1;w# Wذ{y_2'ڭCx{_CK.Ϡ>9#`X+ Vݘ;҂>Фg)YXC5=ƌ4׋h+ qM<&('j,#^'XU_<@2|N5QXX 9Y;Sxǁo{9#FQx.y7d,I|/Roz_w 93ޛh7|2RWd7贊oTNZ>Ww'j\N0`0Ťx .a3;M;}l6wO<+ tu{fzT;ߞt\¢-$kK+UW=>-]>!1\Gd,˗q%Ibc{8`e8^2/C/sSJ!|p #jR3了\w Laxz]k+Ivۻ%ǞdjmxN$mJJJطǡvj Q!" cYK/);$I͸JamI$D;A[J@{675$7וSERSI$)))Ǟ#$IDo=i%v$I1,к שKO$I;+))ay':m"I$I[,I$I$I$I %%I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I "70ac$I$I$IZaŲZݾZ_Q$I$I$I$I%%I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`dҴI&=!I$I$IZX]ʻ|{M?h(c>m9vu47&#~!E;pqɐԙ~ϼĵ{%%I$I$I?dwU6Q ҈ïdfe݌?/AsI$I$I$f ;+eŬBK|2-ZL :ss8nmhX'Ji4{.X zwIY9sw?/XN AN\p57^.cmArn\ؿvjHlD)ڣq:&S8^b9mOA2ȌŜux&8.M*f}2wSs`i|Z9B.?uokF{>F;I$I$I$m!V-Ɵ{3էǁB7᰾'k2ы+^}ҏHHHa8}g]C~h>!x^==wHe ;?ӇkS&WQԆ\1_ 6n MWeKY֞B a%B異4b ed6iIf v܏k)KO%A}/Na洎L`xNssѴ/필$I$I$IXWNFzyCRNxD$%r*nC[AJ("^^شXzm[]cys|NK"mOzױL|cݚD_OBJiITI./`ŪR VvL"%콸WcE,]IqXªr_I$I$I$m1g*Sk/` }/ߓ#nl} <. gҐjė}E˗] :>篿K{3ˉ,W>6!'QI#r6F_,#NO>fxrX0ܟό&VPH/s9e 2Y1f\goN||&k%I$I$Ił^'&'QK$I$I$Iku\Z$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJ$I$I$I %I$I$I$)AK$I$I$IR0$I$I$Ia,I$I$I$I X$I$I$I$I$I$I$%`I$I$I$IJۄ&!I$I$I$$-N3v"yGʾαW}ttP8/ќ6/K͎v~Gѷ>8~MIޖ9P>F:{&ިYWj ADHߕ_ÊxH/߯)oהEկֵ~B*SwXaHgmk{vzTkPN/k7z,\xDW9H$I$I `x>C!Z6_NVf2+ѷ2jRBwC-˧ ;%t7i1{ o.^eo.r_s MO/@oa{3ӛ.#@r*a!pE86]8A*j04Praa VA@HApP)#jwTp8Wk{7Ə5 jn^: !? +xcFdF[>)7h%,(bZHvg2A6ogfIO/U/%Пց\Ƣ6EEy6{}b}~8t>LgK ~E^$I$I1)+`\%g/`Š x`z H8 \'ʲҾA]~?ϿCZ[?"=9|{[FNfk/g,bw<@ƍ}|Ukg'ܟ .9ú搼z_wst3-j? IDAT*p7_mm;/B?en-Ph o>4/,iG.}{w$'+U;`F i\88a}9N4V2çc؜5HvzsVLcn Az;bzu@`%3>^cjrtHK>fzQH.Ք/Y{3|Z\ J Bd:o4RBU6+z3Ԁ`slxYkgWjĕpZVqX0 _ھ AdmpN^L?\n*L 60#ym "²F#z$\_.4a9 ) Yݹ>iLR#ɾL , (;:fd㋋ߡ5̥'Ѥ7~SIځ9wBր{Uݙo`O7-oxJDž|Δ,ַ)ڇ_j %.$g(h+)1mn}z+BޙyzW 9gh礄5´uݏNWUe9,;~oȂPw^,m% $J?Q H'#9lmq\̹rh 8t<<nnMGs8eG.u=![7;5:MseRI[4ao616ܯoI]6 zz7}kθcUqe+5 ^CS\oܓ̦%3xNĒ?JrnBP0g,")M;_!ř|wQ.!?{6%odr>E{̓:GEs4Z#RYu)s VT2ɩaUpk53uf҆aXVZBeZ8\'xŲƱCZ57Rі Rk~"vڬ \:Wζū9q*C!wM\k,Z@oh)5\Wa̐c1k [o'̑'N-h"}ZJ^IUlIoU?|JSglL"Xx}_6mLQ| ]np$SjAi,:#1VA#ު'IQ4FX'F v܂j[@%vTK$ޤ+K:ݳI{1|4?aK' 4p׎`Kݼk}"͢MIv)?eCҳ`봿]R+=a)nA 7ՙDRy|Z%!qf*%S_&d4,ZEv6(߷?e-ڒqqU3SgS:&XmA9 >#ю4mD8)5zga4;1ﶫXC:ݎhXLx3Kn~G|=AfqbMhܷ79(8VY|O^!,حeV26YrqDMϋ_[h/s˦ٗx%ozUcrs}BZY:yF1\ ڇesӈ$oQײ೯h]dթXd h}!uYuB_׈0N!m|Y8$~_/gcZ)& zʔ^,|mo|h72-!us.,I$Imu=g:*?)›zfyq`%Ș~HO@}2;z9e4pFz,.fzW7^WyKd?1| Iw;a2O)o_=+ʁi~oGtN?_]1)(q1_I7 vd!,`߾q4ux?/5Xص91\v9 Իe֬p-ͭ:e0u6TV aP1sbF.8a<8A, !,&\TaKkBh2۵mZW^xmUauB,E]G֛&0^:2^7_ʕm "e@ElhǠ^u^g<͂‹ᙳv acOf+&Lu:]O4%4Tވjg3]R֬>䰤_S:_aL⠐eeO'0#py.)@tFaeH,3%LlwF:)8IKvI ,%uEHږ|sрWIީ3^Sp}GD(:/}Gş3@wՖh{So٬p 켉4yv4{oI$I$m `bӞgXfYX:YCHaa&"A=gڿ+Z3`KܲW.8>5Ե[K'QoJ/>{F(jךԷHd},r{=d#*vR4^}6]Ν(iיwUJfuM \ {ՠ ^DAXe#Ԁ{t ړdf67N6 R3uY%{-nw6#wD."AlQYrRt7 郑0b,M}Q̷T)oxu%ɞ~>(Jڑ`})(!󙡴-?0 s?iT)^ɒ$I$Io&,g,Uf II^3.272g,5B̚GsF˩<STo=$7xա=y,e\-\7vwMɺf"qe_5m_T4\B:X3wvmh ]׆Ae oRUׇUAž cͽjAuTնiwMkXj_k aPD*Hd8*?X9/0_l_M($koUH+&W ʩ7w;EΜi4_H4zy? M'`iYcƺ_h If%G2PNփUB 76 8e]gu$II=E` ezN1Lԉïdib4YB-3Ғ%r.u\wݑv._H?s%f\M_qv@s%;/$|d\1Bu6 F6bY1ets5ybInonkz?oֹo,Hd,>PΡet_I$I$m5"vA-a҆hANNM7O+;f'י;]SJ%4i٘)՟3wIj6=slHW{lUػ(ْM B %"+Z TP 6콡xUDE{ }wg~&$ z}^ć3g3L(e]Bqx?$uʋD&t2ĵmޮݨ(V [e HeR/8"(JiL\‘'Rf`בQM9vG֮Ϯŋ2n99V~ŢxȱY˂E}oNlAI0x/wȫᵫXug^1sI\%ߕ³k1|I Jّp^'m[ED?(lۇfvIg_ɖJn^D=7`q9n[pi5*zeiNj51^{yM߳u^_ޣLo7ô0Lo?g}#k3}}m6eXuo}{i} n\MF?ynf]Կle2AEq݁ȝyn"bC:'&qٷI +f>׽a0:3G31o'el8V6𚘡!ll]CXboJ퀁éڰ>FMةޝZ`oEE{E`` KxffgY/3Ӈ9~=X+4zV6Fz: +q.y.YK}u8_ח(LDm7sm1-h/2,f' M*>cG5 j?5ŸFcc°GwuoGJz۝Rk3!5?a;(!~,6$[Nb)ax=FN! 8݈mhpKafW:lĞ kXk6%nr59󯗯!*bos(~'K#n Nim$[fLv®ivf^TENlc*=lg| )nkj7k[z7I7Բ~j q_-__2}L׿giuϵꊴb_+Z6XvsXͷ5`x uriṰ+T׿g}5,1 ?پ f s,͆e0 ?kw߬d#,`iP}?%8d&E#ĖA#xA:+6fp䱓OuO`+v!;+R&EӉeQt8vwsbŤS<5ؾ./jl" ;G~B O^BZYqb1դ1ݟQ2n*&gS?%nzh!0ee_O^Uښ]&> s2n?Ƴl+h0ür%Yg]RA3ȜUj9-†m٫e~[FOB8?zm&sbۧ?\EԌDz,yRaU0k}v3*9BvFƧ.jpڱjw`$QSF,i5-3ax|Nv?;Vl*Ռaq1 "7E„$-DDDDDDDDD#;/}TZN'V~?^/g8#C0aAv߈}7XvsW[Nv:ōٵw1T-or]ӬJx֬XZ~v۱vc9 Р[?c7p)oq}AcKZ-moYF@}/ `k_ɱ}˯̤1=2n2ϡ5pv{2y6FTzEKAqׂU]=!̭P_v40LLY8[7~sSf_qnFmoѠ\LMA7|3`d_ 641LmiÎa3| {׻ހmpoۆX77i} 5g#7, `incxŷz^˺'YtJ""""""""""r `߁*?׿TrfZglTn4}F6=""""""""""$lW'ȟΦ.iTi!Ti!Ti!Ti!Ti!Ti!Ti!Ti!Ti!TgH")PGH!yh>ys0Ļĝ?"7C\6~O\ >XNN̹*ȶ}X0)Ce(!Gw˹ɶLDDD IDATDDDDDDD:tYaV='*:n$Ntgr'{6XGgǝ.NE^>>qQ{`V"˟}3PTO0_}sfz䉸ߧ>͚[wFQ5놿OrSH{Ĺ3JL{|kPL|RRFQw{1grrjPW̏&*F=/aKHugh[u3۳2һfPn!tVRBbe3aʇlpԷ+ F |ɳ>fStvD7G bOa틴b1._ls"v=gHY=ŔWQs(t.WW&Όcʀ|g&UTC?΢ {:cDN^^a:EwL|]nK.bs]軓ƪ&f4{-U6WmGljeF"{/9Y7~Ie㗆InROBlO QIh6lLvy3$hT:20O{=䈇=IFv l^fפMyRc+4=ę3ITCMxw {HqsZT$O୼]bEwL ;cv]7=ƚWs6!b33yfq.n-6x[ƙqno9znxvmdsJJ/`,=+Ge+o^ql1>"؋nÌjMqs>xmys|ɵX0[>Vž8L\>I7L=.9SH[Zبskٱ*WlⲽcҎ ;t3_k'o,8"-fUpҖ{~to'y'pū]ai7ߒ 0[ro-Ӷmw?ɛ_-dU<7s,mOघx{8v LϽo;.k,^B_Կ72/fx2}.n"chco"m8.У2iE5?c3\Çۃy8f}3F^䷹G mOM'|ݟqu cDz .3s7}9׀mT7>Nu=gg?p @yʰ;'M;Sx'R(>DoGn lA'~#PUB 5ۨT^?Ity%>S2N$t|gK+"w<?z"NFǏR?0p ql+UT=4\Lk +bκ/$'d%ǒPx&dړZp!o?w {z]a}0pmmXV{ul7[^k;I3gG {`3Oo.:6ώbHvJs},x*w}ك]پy~=|0n =OިAl=;+*!qL|!_B w Gn/b'c(_c39[;'t↱7bO8qIw2z{naJv,g#ŏ~OSMfMԜpYcafrVN" Rb)>I=I,L'䥘lVBYYYͷ Ϻ/.jSj d󕝰jXF5J\9)0nlO [0xr'Od>6?o3h/ SՆjekmgT]sUN`?ȁLrG  K{;gY'+(wTFɴxo;:';|A"={রj!A͵J;-t'EDo=Ѹ4{^cqȫ#(5/[9T]|y]1vWJO1spz~Yz k>ھcY3T"PI۟s҅}8{(˯q'Djߑ>b -=uJ0WvK qƐ̱LMc34 ;*+K[<nDRzHv֎UG$|?Ђc.T'Ct`6#d)Aߐ>M;MϬlp UXDAˣ8e^qd{UMfv_Z}㉞/ """""""̽OWܱ%ggZ 'hr"{oSǣfEXܧ9O؈/0beG 7ǝl܂o^/s{eN\o Y/q^`[H97߻6c?93z>yMo:Cone,z&`8b8O}}a!MMHy]6Hn{5n\ 3]d vϜ@_$>ʞ_'|iǞ+H^Bڌ{9H6 ``uFA՝m fO/IݒHk0FEX/`ݴ.H\#<kDڭsڿXGj4h0? a%Q]ݟَtkK3TLۊx+oKj򃪈~a݇gptpdlz 6+? ɏ l梻;Nťz@^b_|6clxCe1''usvYgnv& Mݏ]d*/xBS9"0|OYӍyUh]EJӨhƒ|ia?[禳p>{3*ŊH$YxϦs$v"E8]QxPA{1;j a ox0=,  j{LcC7hhuZƏxivLX[v^Җ$_2oYNTZP<O[;'i_k+)tK^Ŭ|ٛ[_|[NN"`74}5}lNbTn\jYD7#X1Eo3!9=BH\e:O {gv<3=KhW F4G"ugڐe"'$ec#|n!ؖa_ヨ$ёov9s6[TAѝO*~7v̧Ø%OAFuؗ, D8d2O;@tډ\^MM]vJfEFl\7ct81Z5}lM{3Hdϰ5OS}Tء|?;1T}GϑOaK:um|KQ9>y>63>spgxof͉Īn`=L_vDDDDDDDoIDa^X|G\ڇs|5m%!TȺ.{7ALphߍ$w^6bcpv^S:NX1D:Uɞ|_#(سw߿yWDj #9Pf ^l8bIE Vf@V:Bۓ{`*̠sȯ/YPM_U6{%> ?&%`8=#\xb|:%/% 0:Pn1?=w9Q{/ ~2fS:X"' w;8X@dǰn:mh}| "PO U fO *:ӨLJԪJ_߮YModAyrR}Dž9tì78jiQ~9uӞfoIM6R\֠[Cy WdU}[߸UZoyf/CXe%j6bb#g_R"c E]Ͼ/ip(-wz``ac}ef+yH=e?2;p+,b |IEjb‰ f)lT 3 6$Q. pMZ=|j1"ac`k,A 0"(NM0[&V@Av`=AtOMvFD+9µZ`h;7M ]Cu3X}{[X8^# OhD&=IP^Su"k0*C3TV`;|Ƈax@]<\'`o[غl!|vjOOd#v[u>1f_{&}@YD*nz5&CM J@OD_G" =*vRslew`؉Xnað(hr|;澹4Ov-/g|6p"YC95ucي)rǑo=$ ^d赜7c #mr]Q_2<)jAiT N LlLƎ&PT q8$RT\6o ťןE]y)(9O6Nc먳͝@/yVIщ$8a)ȤDKw6Fy@_LGѩ]>Uѻ}oX0<|!ض|SL&aWRižk~FgŚ`e]5e7QVF.b {>cZ=~mm!rRtl; x|z8(Q_7~ILTiPէ('Kf[{{g<,\L耣t@(V+,BΚ@d 4Kaê{;o^3p:-Opy\T^oD˱5ulM7 @Lͬ{%aLz$O~7 ߦ˝V^Qa)=wHQ`rATfq!%qwHlٿO:֛=6~ٸسA?=sO7ݏ}@moJ5/ y$EcG>g{n8{I}¬%d~U}A#(p7]_݊-,~zω6>5&m7qV&dd:= rT8^"r^ #{'`LaX2 S#4stm'ّ|wc۸ Vi+4ql5͏'ýVMR} 66ZqSrԱ~j fݒmp\0-,p) !u=(4S|T,(~l?7O""""""""RǦ.h*ɒx.E ԮbAz8pDv䢛%qٗ|W73xWx NK'f#1]B|HJ{ЧF2RG? XGڣrFB=&1tQ+?hW6Fz: +q.y.Ϯ"$'u~x~czShQ7޾``Uc(w㍊jزcpS,lex ;W?ĺ%O{h76Q5iwTk50$񫦇Z-/㖓S hwL_,ڌ׻8>ap%tcuxqA.k;8֛96kKMI~VmXM؁(hz±oۂ fQq6 T{cNt6~KgWLMFb>zAvx ׶]xvwWXUX&iu7?D #l[Qnk$U| n0b)F&v]vXMBDXoƆci1rwHE;OoT7s\ٻJNBqXDXH鉝oXNxI;UMԏP { -[+"W͋{"PM'HժϭZ$ŵFVzwʢ ih>7wl5Ұ w}뙾69j݇l}ˏĿm?(SXIa}ft=Y7fsWTCc/w~݉J>$X&Ԟ1cjciƑ 1}lxqfO2? K*ts5(+imăDMFl< qĊI6xk2˱}]^KyXӥqV4L,$9.<؊¨m!z֏ÊRafj g/fٱm7o~z~"lqVӈ/[N' ,iZuu3/ DƴoFASsbۧ?\EԌDzXoKwl;d4߿a7^޳U0ާ܏GE;2vB<vQ<:g׹̛ ]س>>uj o]J| Bzs&Hّ-RPmby6>S,泒BNqrC:4h8?g4/ > t0֍ێk7$+eǀ;OYD~c`.Y/Wxqyesby ?O%hV¶zu-q7 wbafW@҇m8mog&?9MByX7Bl ?BJk* v޷4gZ?݃'|ƚ_WwNaieNۛl{!v;x̑3(+sּF/Gel,Wi7wd̝Na}?={9{Id`YMy[ҧweӿfoذJ~ ōsU$ukÛhߍZoGAb5gUf'qSTէUCI|70͌8{em_/6ӉUǶ瘴cΦ 7R:!X_FqbGB7^w+KW6s6O);#=/:"""""""eyӜN]986FTz.x{ksɼ}[J>l|b/y8F@H Ä~c[-Ύջ|69g⫝̸m_y ægyHkOTv>ɶm8`s6j Iy Eu"2Ϥ|^S~WB)>];ۃw _n>U~}~=,6orZ/`݄ ZAt?KOU##;awݤ<8eE~6c9C[h-3w0Bz^…vL!ZY?ׇuMChվ'5߽EFޟyރĪ0H~7=~֮FCGwH[7<_8їƨ+"""""""k,""?a'떳8,IU:>}q?I:GDDDDDDDDDWmE""""""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- """"""""""""""- ̌ (4 232 ־quadefHF}Z5FVfJh?=8?;2 Mo? * Lk$: F$6_ _EVfCcůo 5MVfcooCVfD5ů%+3+#oK/ L#+3"H/ 폟8>&) Ή o?+q|\rY(?~ZxVdefpRxh a㏶ŏ iuY8>=?* >3%  nM+23j&.5Ytr5w쏷 r6BVf73|6w2ڐAkQxq|^T23w?l?ǣS' 8wH'+3 qK_AVf ?w-i&rW?y@<ğp( wsׇx뽀ڟ ]m͌9*ݟ^ ]mrTz1 .q?w=z^5; wLi?ʟsڱc8N{4 wi61 ~?=q"|9꾀u?w Qw 뎀Ȅ부u?>4qji)qsW9& w ǯ r(( w_Q{c9o@/~J@N{?w&宀6՟n"8G=]|!s- w=w?QrKxW+# wᏧ䮷+% w'9-! w۟r8q;_]sWh@DrĿm]L}L}L}L}L}L}}N t b?KO3EDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZEDDDDDDDDDDDDDZƶ Ovpe """""""""""""#$2;ځ 0 IMalO(uD|k^Oo'ҙ«#iw nAm8Zunq AF'ǒV{HʽUߍ%DQSː*XNXI*>BHh8֖11hd1ag.6\Zɣv.HUr/3pre>vbb2VHJ2xȘ EpTu jp;y$@Z6'|&ZDD$jtWUPU[Ƹs#S+n/n/=b"YK}ئbn \k/KU1qb]cD`; fYlvPS[Qb>.RDc[Yf >Ȏ*AdUz1U艠Kg^ oݲ TkX[XިjmJj԰ V-}jZhP \_x-jL >1YVQM99}%""""""""""""!ܑ\U V-o1KʨlA g9+y2f: 4U{ɘgwנG|kl_8x}g +D_j{4~pZگIlCZ7r$6,?_U,KQ$m3l's1rsb@lL.$H0@k.A\b' @xf<]U=ať>Wۯj778r^1fW!ն^ݎ+q?Pf1#Y!nxPEQ_Î7ᶥ=ߋ磫Vk+Ə:Q^z<,V{1w6USE."\1}|-V_WF##t8ܤ6wVV_ _ӫ_`Qw=Wbz?o6ߞp"BP\nGJv#"V`닍efzR1UWo^6◫#jPލ{:Ň#:sџGet4vy#`v=`{[_sGݱRZ\S}^͍#φ+1WfωZaH  0@"`D IDATpwtĭozF皸v=r{G.MNs`lJL=zwWgW]oݹ==qyl/!ML3vy|"&uuu h41\G977֣t!ML34pi06֢R.QU4fl#=RDpRttvFkkӤ`8#\C#>oj5 sVZv -ŨU4{.㧆͍9>oe5! gdnv07㱺X$P<}Zvwcme9Kn`R^j%z.\x꿏_G޾ͭ71?7#:屘zO9;;۱?$. FGGgl|,DυRl90GK==b‘R"Bލ<`8|!ꑯ/WʱFbfсJz#V335cxJ|<`8j5 s31\\.XY^jpHca~Z[Q,cmmRhylmnDwwK=V14\|.C177kF4O=ߩ|RZFsٽ?i8yoݹSTF#ggs `8%|W#˲Ysk[[H!$BH I Lݼ_1C1R{`\qȏÏu[ߎ_7gChR[ш,MVFR;\.. VZ;j5Sc||DlֈpER>#b~n.sRَگ㝝T+s‰UsXn__Ŗbܸ};?щ)8;cz-c13HsHDD/0@BYN-o#.^cWbjቮV14zfs `xI]=}h|ei1=93~^zln{r hD!$BHD4v' m?os8vf,->y~GZm1\ ceyi}]=Q( :::";?07oݎَjm1qZODZ. H+˯s{d!~'` ҷ5bوcSykjTCs3SzhW_D;;_vee9&nlkz=,w>4g\s񉘞ߣ4kkQ)_xmk[[\v#""7C#Ww'mO³=:6=Q>}<+K1<2MuB`7؝F.Vs~r,FJ/q|\Kc{_աkcp3yֺg=ɯLMNĕss9-xsf=%.Fok-^i'eYtuv٨0_6[åCcr9fg?6%?{~/-> @D?'ѱ4_y+|+vbfzj=nz+cel֘~(vww=.GGgglomd>a/tH  0@"`i%okqvtuv>(Z[[Om]Yw?~ߏ:4o|u!|E0lE#6,|ᵵj5jSJj-jxj5 Wa8d6kn<<+-Iα+1hHlGynݾ#c1T{{{/Wk^Y^>5X(AED.GV| Np$gtb\it,fsα݉$f&؄K8a=484+K==XsGOυ$sE>|/AHYN-#P͞sJqXs\RD>uS1>}I6.G-˲x◿YS* 4='7!$BH  0z.~?nGWgש#e{G>TeY}YQz{5ߺvwte 8[|\.. ]]шͭS[p VPhnD_DDx{KRVDR}650'|^@XV#b1kZ!~v>r屘z:ƯԣS[GP޾Xx<0bpx(fOy/pwWgWdY{&8s,~?[mq#_?\*CW݌b۱37;ÑϝTYI7QvOfU,FSNnmnzӮ;˲x◿YDD\bō,kD#ÍzdYxsGLS.\=.7PY48^Z|rbk9ɹ9]N$o$BH  0@"`έsX~ǚclJ\xH^v#n}7ݸZ_G[{{\y+?0}ޏ\.a 5b뜬VFZ=眛_}Zى"VV?~x8K>,8V#b1kZ!~vkgg;jGJqcwGGg=( 69"4WըZs7VWVb{kXWލ<4>;3cc1Nt>1g_NkZKWؿќ޽uufkK%(F!`Wr|bQtW{g5RJvb !g 찱l@sOs5ׇoޱy;g_ qkE__!_мl|ff?9 9$ONF]^ T1<2bȗlxd$*];wFc'$'=g @; ŷ]|76nc'vϬoō|b;"Iюӡw}IXX/],ce~y_w/> 0\ X''kQ5Xs)돩(Jۈ\mc} %)!H 0@J)܍3~^~糙L0=`?měT 2Hb4ݻxŗQ,NxŗVWVh9r\WwL^ɾ?,̿xS:-Ə?ãSkG//-dVxssdR}{`/FFn~R`Rcc}=Z֩VgnR) Bފk'{ﳸ:SOw@ @jˑeOes-߹a~>j]3Cssd7G1<2r,ߥlFW>0q(N}qwo4v"I( tJz:dߵ8<<<_{\X̧7@Z/>w//ɹv|Fko=wi_0=?>9j=x= .Q1>o %)!H 0oS s-vRdѓd$Ihܑ.V /~t?>9U;S3I 0t@I@i J$I4v|!HoY{a~>g>-J##n1V$-Ϟzp  sx.nN,Xl&ՕřWίpA|oH 0@JRB `E$?_,^y_.VXoS WIHW1==s}4v@iµZf4[l: 0^_͠xŗQ,kD @-οkA m4by~ӏ?ãjGG[WN.ѸQ( }}]h5[5" RL&>flnnxR/:^|utr9vHVoVcR^T"Q9`H./GDhV|ԕ>''WKL?;X׌OC Ϟ` %)! 5>$I>our2'ٌ؎$ZS3]I+119uZjW/X,98׈Aͮ]sZ  Ǎ YfW/F :U5c6s[qpվ7ףl94׈~lFWvͭ_ͽ}}188tf}pp(:r9CsYcRl{q|L[##QJ##];ws`i퓓u{݋osr=Vgֿ?g/_g0@J$O=o@0@JRB `xx.$Ԛ3PIDAT$Iϝ{wXjXXcl|,5~e\k/184P 07k18477m+mt?2v1-_%sXZ6ףޟ>:>-29lmmEbO=Z.G6ﳼdd2C`8魷o߹|_~VoVcRu !gp>{݋o{=Y׬./; `H>{6 ` %)! UŸwAOZ >$IRyo`R6= _D+zE6#r8IP^O{nrLLNuO2RHX\艹߬pHxG=W# ʷbgk+ztz2$'Q_Z"_GkˑeOes-]cpp(ϬON:3]0zxkjۨ 1=so߾s7 Sk|_ܾs5*j ZXe"k$yyQl&ՕG4hjo7vw lR\|0@JRB `Г2IE6suW$\y_Tur2'ά߻ t^^TVcmu-Z''aRؙ{aULOϤ9 9\.Fncmu0~/byxŗv;Vcbr^7cph矷SAÏ?ãT ~jYḑqj}quL-`0=P(DT 8h5[wي YZ(b|Rׯ F沩w6ru"_GbT$=@$Ia;wKż }ZoZXj>m>\$ѓ?z$wGWKr|;60@J` %)!H 0pff/T#Ixx#udѓd?l {Dر>Y^&0IY[J0*g<+$7Gˑϣ2q^7cphú"`!Řάvs[S2`!?mDTBpj}{{+ڊ- ,GmfP_RL&L =؉$Ib4ZZᑑk_Պ71^zX]3=/{q߿Z!]oD1>o %)!H 0xɎ^#ģ's$=9v$GO"\YIs]=W5~N:f2qklbZ׮Y_ZZB5^{Jo77;r?]3\L,-57ڈbBkX_?88tvPR`p^G>h|Cߑ{Tl,~ 0W6= ڍN$I|!=J##N( D$i8="p͕^w/>{{׿k7vϯT'Ϭ'$gamvje]R_2H>{6;RB ` %\$I9xJ%&63C×rݏsnZL&=L&p_B!.\Hk}wl&cu޵k"kf4[UuroVcRpoh72컛 ~b)~eGjGDLLbey9NN]l,?vxxGG]sue%n#]Iumw7 VxyG/;RF>Cfk>rf龗عPbLj_pY}wKί0sm5[ޑSXZx{Noّڛ?mDeb" B^ZB__ܾYDD.Oqst4'""u&Ծ;E 088GGGqp{l ;>ob1|lm}OOl;vGxv3Z}?|ff/-Ãxg9 N)4BZᑑs-^6= vL&*㱼\ݾs7 so4v"I( ԙ襾Ϟp$?'s|!ngw;}f}|+ngWKQcw;~tsvS}? W<6\ilGcgC>H  ` %\JLT'Lģ'sz$I<| f2\d2C\ L&nJ{TK:nFu5Hb4}ěT'Zr\.#7˱|.̿ X]Yescpn188o֮t;R{eK{xx{12rBuK1Y9=L ]<Z/T*ƥŅQXxFK( e6WX|>[[o;R?Hlvxp/l,~u뽯v5܈J|tƿAJզgcaU}q7P$IY.1VD61`I@iAgu2Z7Ӌ$iG;Hnc'v/G>_{k'I=+kXs-8::B'#morRB `R( [5쮡00Xٌfy00^mc}9#O˽G&XZxm )"O/5)!H 0@JRB ` %)!H 0@JRB ` %)!H 0@JRB `AoTIENDB`mpire-2.10.2/docs/usage/workerpool/000077500000000000000000000000001461637447300171665ustar00rootroot00000000000000mpire-2.10.2/docs/usage/workerpool/cpu_pinning.rst000066400000000000000000000021661461637447300222360ustar00rootroot00000000000000CPU pinning =========== You can pin the child processes of :obj:`mpire.WorkerPool` to specific CPUs by using the ``cpu_ids`` parameter in the constructor: .. code-block:: python # Pin the two child processes to CPUs 2 and 3 with WorkerPool(n_jobs=2, cpu_ids=[2, 3]) as pool: ... # Pin the child processes to CPUs 40-59 with WorkerPool(n_jobs=20, cpu_ids=list(range(40, 60))) as pool: ... # All child processes have to share a single core: with WorkerPool(n_jobs=4, cpu_ids=[0]) as pool: ... # All child processes have to share multiple cores, namely 4-7: with WorkerPool(n_jobs=4, cpu_ids=[[4, 5, 6, 7]]) as pool: ... # Each child process can use two distinctive cores: with WorkerPool(n_jobs=4, cpu_ids=[[0, 1], [2, 3], [4, 5], [6, 7]]) as pool: ... CPU IDs have to be positive integers, not exceeding the number of CPUs available (which can be retrieved by using :meth:`mpire.cpu_count`). Use ``None`` to disable CPU pinning (which is the default). .. note:: Pinning processes to CPU IDs doesn't work when using threading or when you're on macOS.mpire-2.10.2/docs/usage/workerpool/dill.rst000066400000000000000000000021241461637447300206430ustar00rootroot00000000000000.. _use_dill: Dill ==== .. contents:: Contents :depth: 2 :local: For some functions or tasks it can be useful to not rely on pickle, but on some more powerful serialization backends like dill_. ``dill`` isn't installed by default. See :ref:`dilldep` for more information on installing the dependencies. One specific example where ``dill`` shines is when using start method ``spawn`` (the default on Windows) in combination with iPython or Jupyter notebooks. ``dill`` enables parallelizing more exotic objects like lambdas and functions defined in iPython and Jupyter notebooks. For all benefits of ``dill``, please refer to the `dill documentation`_. Once the dependencies have been installed, you can enable it using the ``use_dill`` flag: .. code-block:: python with WorkerPool(n_jobs=4, use_dill=True) as pool: ... .. note:: When using ``dill`` it can potentially slow down processing. This is the cost of having a more reliable and powerful serialization backend. .. _dill: https://pypi.org/project/dill/ .. _dill documentation: https://github.com/uqfoundation/dill mpire-2.10.2/docs/usage/workerpool/index.rst000066400000000000000000000004301461637447300210240ustar00rootroot00000000000000WorkerPool ========== This section describes how to setup a :obj:`mpire.WorkerPool` instance. .. toctree:: :maxdepth: 1 setup start_method cpu_pinning worker_id shared_objects worker_state keep_alive worker_insights dill order_tasks mpire-2.10.2/docs/usage/workerpool/keep_alive.rst000066400000000000000000000056531461637447300220350ustar00rootroot00000000000000.. _keep_alive: Keep alive ========== .. contents:: Contents :depth: 2 :local: By default, workers are restarted on each ``map`` call. This is done to clean up resources as quickly as possible when the work is done. Workers can be kept alive in between consecutive map calls using the ``keep_alive`` flag. This is useful when your workers have a long startup time and you need to call one of the map functions multiple times. .. code-block:: python def foo(x): pass with WorkerPool(n_jobs=4, keep_alive=True) as pool: pool.map(task, range(100)) pool.map(task, range(100)) # Workers are reused here Instead of passing the flag to the :obj:`mpire.WorkerPool` constructor you can also make use of :meth:`mpire.WorkerPool.set_keep_alive`: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.map(task, range(100)) pool.map(task, range(100)) # Workers are restarted pool.set_keep_alive() pool.map(task, range(100)) # Workers are reused here Caveats ------- Changing some WorkerPool init parameters do require a restart. These include ``pass_worker_id``, ``shared_objects``, and ``use_worker_state``. Keeping workers alive works even when the function to be called or any other parameter passed on to the ``map`` function changes. However, when you're changing either the ``worker_init`` and/or ``worker_exit`` function while ``keep_alive`` is enabled, you need to be aware this can have undesired side-effects. ``worker_init`` functions are only executed when a worker is started and ``worker_exit`` functions when a worker is terminated. When ``keep_alive`` is enabled, workers aren't restarted in between consecutive ``map`` calls, so those functions are not called. .. code-block:: python def init_func_1(): pass def exit_func_1(): pass def init_func_2(): pass def init_func_2(): pass with WorkerPool(n_jobs=4, keep_alive=True) as pool: pool.map(task, range(100), worker_init=init_func_1, worker_exit=exit_func_1) pool.map(task, range(100), worker_init=init_func_2, worker_exit=exit_func_2) In the above example ``init_func_1`` is called for each worker when the workers are started. After the first ``map`` call ``exit_func_1`` is not called because workers are kept alive. During the second ``map`` call ``init_func_2`` isn't called as well, because the workers are still alive. When exiting the context manager the workers are shut down and ``exit_func_2`` is called. It gets even trickier when you also enable ``worker_lifespan``. In this scenario during the first ``map`` call a worker could've reached its maximum lifespan and is forced to restart, while others haven't. The exit function of the worker to be restarted is called (i.e., ``exit_func_1``). When calling ``map`` for the second time and the exit function is changed, the other workers will execute the new exit function when they need to be restarted (i.e., ``exit_func_2``). mpire-2.10.2/docs/usage/workerpool/order_tasks.rst000066400000000000000000000030261461637447300222410ustar00rootroot00000000000000Order tasks =========== .. contents:: Contents :depth: 2 :local: In some settings it can be useful to supply the tasks to workers in a round-robin fashion. This means worker 0 will get task 0, worker 1 will get task 1, etc. After each worker got a task, we start with worker 0 again instead of picking the worker that has most recently completed a task. When the chunk size is larger than 1, the tasks are distributed to the workers in order, but in chunks. I.e., when ``chunk_size=3`` tasks 0, 1, and 2 will be assigned to worker 0, tasks 3, 4, and 5 to worker 1, and so on. When ``keep_alive`` is set to ``True`` and the second ``map`` call is made, MPIRE resets the worker order and starts at worker 0 again. .. warning:: When tasks vary in execution time, the default task scheduler makes sure each worker is busy for approximately the same amount of time. This can mean that some workers execute more tasks than others. When using ``order_tasks`` this is no longer the case and therefore the total execution time is likely to be higher. You can enable/disable task ordering by setting the ``order_tasks`` flag: .. code-block:: python def task(x): pass with WorkerPool(n_jobs=4, order_tasks=True) as pool: pool.map(task, range(10)) Instead of passing the flag to the :obj:`mpire.WorkerPool` constructor you can also make use of :meth:`mpire.WorkerPool.set_order_tasks`: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.set_order_tasks() pool.map(task, range(10)) mpire-2.10.2/docs/usage/workerpool/setup.rst000066400000000000000000000070371461637447300210670ustar00rootroot00000000000000Starting a WorkerPool ===================== .. contents:: Contents :depth: 2 :local: The :obj:`mpire.WorkerPool` class controls a pool of worker processes similarly to a ``multiprocessing.Pool``. It contains all the ``map`` like functions (with the addition of :meth:`mpire.WorkerPool.map_unordered`), together with the ``apply`` and ``apply_async`` functions (see :ref:`apply-family`). An :obj:`mpire.WorkerPool` can be started in two different ways. The first and recommended way to do so is using a context manager: .. code-block:: python from mpire import WorkerPool # Start a pool of 4 workers with WorkerPool(n_jobs=4) as pool: # Do some processing here pass The ``with`` statement takes care of properly joining/terminating the spawned worker processes after the block has ended. The other way is to do it manually: .. code-block:: python # Start a pool of 4 workers pool = WorkerPool(n_jobs=4) # Do some processing here pass # Only needed when keep_alive=True: # Clean up pool (this will block until all processing has completed) pool.stop_and_join() # or use pool.join() which is an alias of stop_and_join() # In the case you want to kill the processes, even though they are still busy pool.terminate() When using ``n_jobs=None`` MPIRE will spawn as many processes as there are CPUs on your system. Specifying more jobs than you have CPUs is, of course, possible as well. .. warning:: In the manual approach, the results queue should be drained before joining the workers, otherwise you can get a deadlock. If you want to join either way, use :meth:`mpire.WorkerPool.terminate`. For more information, see the warnings in the Python docs here_. .. _here: https://docs.python.org/3/library/multiprocessing.html#pipes-and-queues Nested WorkerPools ------------------ By default, the :obj:`mpire.WorkerPool` class spawns daemon child processes who are not able to create child processes themselves, so nested pools are not allowed. There's an option to create non-daemon child processes to allow for nested structures: .. code-block:: python def job(...) with WorkerPool(n_jobs=4) as p: # Do some work results = p.map(...) with WorkerPool(n_jobs=4, daemon=True, start_method='spawn') as pool: # This will raise an AssertionError telling you daemon processes # can't start child processes pool.map(job, ...) with WorkerPool(n_jobs=4, daemon=False, start_method='spawn') as pool: # This will work just fine pool.map(job, ...) .. note:: Nested pools aren't supported when using threading. .. warning:: Spawning processes is not thread-safe_! Both ``start`` and ``join`` methods of the ``process`` class alter global variables. If you still want to have nested pools, the safest bet is to use ``spawn`` as start method. .. note:: Due to a strange bug in Python, using ``forkserver`` as start method in a nested pool is not allowed when the outer pool is using ``fork``, as the forkserver will not have been started there. For it to work your outer pool will have to have either ``spawn`` or ``forkserver`` as start method. .. warning:: Nested pools aren't production ready. Error handling and keyboard interrupts when using nested pools can, on some rare occassions (~1% of the time), still cause deadlocks. Use at your own risk. When a function is guaranteed to finish successfully, using nested pools is absolutely fine. .. _thread-safe: https://bugs.python.org/issue40860 mpire-2.10.2/docs/usage/workerpool/shared_objects.rst000066400000000000000000000104131461637447300226760ustar00rootroot00000000000000.. _shared_objects: Shared objects ============== .. contents:: Contents :depth: 2 :local: MPIRE allows you to provide shared objects to the workers in a similar way as is possible with the ``multiprocessing.Process`` class. For the start method ``fork`` these shared objects are treated as ``copy-on-write``, which means they are only copied once changes are made to them. Otherwise they share the same memory address. This is convenient if you want to let workers access a large dataset that wouldn't fit in memory when copied multiple times. .. note:: The start method ``fork`` isn't available on Windows, which means copy-on-write isn't supported there. For ``threading`` these shared objects are readable and writable without copies being made. For the start methods ``spawn`` and ``forkserver`` the shared objects are copied once for each worker, in contrast to copying it for each task which is done when using a regular ``multiprocessing.Pool``. .. code-block:: python def task(dataset, x): # Do something with this copy-on-write dataset ... def main(): dataset = ... # Load big dataset with WorkerPool(n_jobs=4, shared_objects=dataset, start_method='fork') as pool: ... = pool.map(task, range(100)) Multiple objects can be provided by placing them, for example, in a tuple container. Apart from sharing regular Python objects between workers, you can also share multiprocessing synchronization primitives such as ``multiprocessing.Lock`` using this method. Objects like these require to be shared through inheritance, which is exactly how shared objects in MPIRE are passed on. .. important:: Shared objects are passed on as the second argument, after the worker ID (when enabled), to the provided function. Instead of passing the shared objects to the :obj:`mpire.WorkerPool` constructor you can also use the :meth:`mpire.WorkerPool.set_shared_objects` function: .. code-block:: python def main(): dataset = ... # Load big dataset with WorkerPool(n_jobs=4, start_method='fork') as pool: pool.set_shared_objects(dataset) ... = pool.map(task, range(100)) Shared objects have to be specified before the workers are started. Workers are started once the first ``map`` call is executed. When ``keep_alive=True`` and the workers are reused, changing the shared objects between two consecutive ``map`` calls won't work. Copy-on-write alternatives -------------------------- When copy-on-write is not available for you, you can also use shared objects to share a ``multiprocessing.Array``, ``multiprocessing.Value``, or another object with ``multiprocessing.Manager``. You can then store results in the same object from multiple processes. However, you should keep the amount of synchronization to a minimum when the resources are protected with a lock, or disable locking if your situation allows it as is shown here: .. code-block:: python from multiprocessing import Array def square_add_and_modulo_with_index(shared_objects, idx, x): # Unpack results containers square_results_container, add_results_container = shared_objects # Square, add and modulo square_results_container[idx] = x * x add_results_container[idx] = x + x return x % 2 def main(): # Use a shared array of size 100 and type float to store the results square_results_container = Array('f', 100, lock=False) add_results_container = Array('f', 100, lock=False) shared_objects = square_results_container, add_results_container with WorkerPool(n_jobs=4, shared_objects=shared_objects) as pool: # Square, add and modulo the results and store them in the results containers modulo_results = pool.map(square_add_and_modulo_with_index, enumerate(range(100)), iterable_len=100) In the example above we create two results containers, one for squaring and for adding the given value, and disable locking for both. Additionally, we also return a value, even though we use shared objects for storing results. We can safely disable locking here as each task writes to a different index in the array, so no race conditions can occur. Disabling locking is, of course, a lot faster than having it enabled. mpire-2.10.2/docs/usage/workerpool/start_method.rst000066400000000000000000000054251461637447300224230ustar00rootroot00000000000000.. _start_methods: Process start method ==================== .. contents:: Contents :depth: 2 :local: The ``multiprocessing`` package allows you to start processes using a few different methods: ``'fork'``, ``'spawn'`` or ``'forkserver'``. Threading is also available by using ``'threading'``. For detailed information on the multiprocessing contexts, please refer to the multiprocessing documentation_ and caveats_ section. In short: fork Copies the parent process such that the child process is effectively identical. This includes copying everything currently in memory. This is sometimes useful, but other times useless or even a serious bottleneck. ``fork`` enables the use of copy-on-write shared objects (see :ref:`shared_objects`). spawn Starts a fresh python interpreter where only those resources necessary are inherited. forkserver First starts a server process (using ``'spawn'``). Whenever a new process is needed the parent process requests the server to fork a new process. threading Starts child threads. Suffers from the Global Interpreter Lock (GIL), but works fine for I/O intensive tasks. For an overview of start method availability and defaults, please refer to the following table: .. list-table:: :header-rows: 1 * - Start method - Available on Unix - Available on Windows * - ``fork`` - Yes (default) - No * - ``spawn`` - Yes - Yes (default) * - ``forkserver`` - Yes - No * - ``threading`` - Yes - Yes Spawn and forkserver -------------------- When using ``spawn`` or ``forkserver`` as start method, be aware that global variables (constants are fine) might have a different value than you might expect. You also have to import packages within the called function: .. code-block:: python import os def failing_job(folder, filename): return os.path.join(folder, filename) # This will fail because 'os' is not copied to the child processes with WorkerPool(n_jobs=2, start_method='spawn') as pool: pool.map(failing_job, [('folder', '0.p3'), ('folder', '1.p3')]) .. code-block:: python def working_job(folder, filename): import os return os.path.join(folder, filename) # This will work with WorkerPool(n_jobs=2, start_method='spawn') as pool: pool.map(working_job, [('folder', '0.p3'), ('folder', '1.p3')]) A lot of effort has been put into making the progress bar, dashboard, and nested pools (with multiple progress bars) work well with ``spawn`` and ``forkserver``. So, everything should work fine. .. _documentation: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods .. _caveats: https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods mpire-2.10.2/docs/usage/workerpool/worker_id.rst000066400000000000000000000034161461637447300217110ustar00rootroot00000000000000.. _workerID: Accessing the worker ID ======================= .. contents:: Contents :depth: 2 :local: Each worker in MPIRE is given an integer ID to distinguish them. Worker #1 will have ID ``0``, #2 will have ID ``1``, etc. Sometimes it can be useful to have access to this ID. By default, the worker ID is not passed on. You can enable/disable this by setting the ``pass_worker_id`` flag: .. code-block:: python def task(worker_id, x): pass with WorkerPool(n_jobs=4, pass_worker_id=True) as pool: pool.map(task, range(10)) .. important:: The worker ID will always be the first argument passed on to the provided function. Instead of passing the flag to the :obj:`mpire.WorkerPool` constructor you can also make use of :meth:`mpire.WorkerPool.pass_on_worker_id`: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.pass_on_worker_id() pool.map(task, range(10)) Elaborate example ----------------- Here's a more elaborate example of using the worker ID together with a shared array, where each worker can only access the element corresponding to its worker ID, making the use of locking unnecessary: .. code-block:: python def square_sum(worker_id, shared_objects, x): # Even though the shared objects is a single container, we 'unpack' it anyway results_container = shared_objects # Square and sum results_container[worker_id] += x * x # Use a shared array of size equal to the number of jobs to store the results results_container = Array('f', 4, lock=False) with WorkerPool(n_jobs=4, shared_objects=results_container, pass_worker_id=True) as pool: # Square the results and store them in the results container pool.map_unordered(square_sum, range(100)) mpire-2.10.2/docs/usage/workerpool/worker_insights.rst000066400000000000000000000076011461637447300231450ustar00rootroot00000000000000.. _worker insights: Worker insights =============== Worker insights gives you insight in your multiprocessing efficiency by tracking worker start up time, waiting time and time spend on executing tasks. Tracking is disabled by default, but can be enabled by setting ``enable_insights``: .. code-block:: python with WorkerPool(n_jobs=4, enable_insights=True) as pool: pool.map(task, range(100)) The overhead is very minimal and you shouldn't really notice it, even on very small tasks. You can view the tracking results using :meth:`mpire.WorkerPool.get_insights` or use :meth:`mpire.WorkerPool.print_insights` to directly print the insights to console: .. code-block:: python import time def sleep_and_square(x): # For illustration purposes time.sleep(x / 1000) return x * x with WorkerPool(n_jobs=4, enable_insights=True) as pool: pool.map(sleep_and_square, range(100)) insights = pool.get_insights() print(insights) # Output: {'n_completed_tasks': [28, 24, 24, 24], 'total_start_up_time': '0:00:00.038', 'total_init_time': '0:00:00', 'total_waiting_time': '0:00:00.798', 'total_working_time': '0:00:04.980', 'total_exit_time': '0:00:00', 'total_time': '0:00:05.816', 'start_up_time': ['0:00:00.010', '0:00:00.008', '0:00:00.008', '0:00:00.011'], 'start_up_time_mean': '0:00:00.009', 'start_up_time_std': '0:00:00.001', 'start_up_ratio': 0.006610452621805033, 'init_time': ['0:00:00', '0:00:00', '0:00:00', '0:00:00'], 'init_time_mean': '0:00:00', 'init_time_std': '0:00:00', 'init_ratio': 0.0, 'waiting_time': ['0:00:00.309', '0:00:00.311', '0:00:00.165', '0:00:00.012'], 'waiting_time_mean': '0:00:00.199', 'waiting_time_std': '0:00:00.123', 'waiting_ratio': 0.13722942739284952, 'working_time': ['0:00:01.142', '0:00:01.135', '0:00:01.278', '0:00:01.423'], 'working_time_mean': '0:00:01.245', 'working_time_std': '0:00:00.117', 'working_ratio': 0.8561601182661567, 'exit_time': ['0:00:00', '0:00:00', '0:00:00', '0:00:00'] 'exit_time_mean': '0:00:00', 'exit_time_std': '0:00:00', 'exit_ratio': 0.0, 'top_5_max_task_durations': ['0:00:00.099', '0:00:00.098', '0:00:00.097', '0:00:00.096', '0:00:00.095'], 'top_5_max_task_args': ['Arg 0: 99', 'Arg 0: 98', 'Arg 0: 97', 'Arg 0: 96', 'Arg 0: 95']} We specified 4 workers, so there are 4 entries in the ``n_completed_tasks``, ``start_up_time``, ``init_time``, ``waiting_time``, ``working_time``, and ``exit_time`` containers. They show per worker the number of completed tasks, the total start up time, the total time spend on the ``worker_init`` function, the total time waiting for new tasks, total time spend on main function, and the total time spend on the ``worker_exit`` function, respectively. The insights also contain mean, standard deviation, and ratio of the tracked time. The ratio is the time for that part divided by the total time. In general, the higher the working ratio the more efficient your multiprocessing setup is. Of course, your setup might still not be optimal because the task itself is inefficient, but timing that is beyond the scope of MPIRE. Additionally, the insights keep track of the top 5 tasks that took the longest to run. The data is split up in two containers: one for the duration and one for the arguments that were passed on to the task function. Both are sorted based on task duration (desc), so index ``0`` of the args list corresponds to index ``0`` of the duration list, etc. When using the MPIRE :ref:`Dashboard` you can track these insights in real-time. See :ref:`Dashboard` for more information. .. note:: When using `imap` or `imap_unordered` you can view the insights during execution. Simply call ``get_insights()`` or ``print_insights()`` inside your loop where you process the results. mpire-2.10.2/docs/usage/workerpool/worker_state.rst000066400000000000000000000053161461637447300224360ustar00rootroot00000000000000.. _worker_state: Worker state ============ .. contents:: Contents :depth: 2 :local: If you want to let each worker have its own state you can use the ``use_worker_state`` flag: .. code-block:: python def task(worker_state, x): if "local_sum" not in worker_state: worker_state["local_sum"] = 0 worker_state["local_sum"] += x with WorkerPool(n_jobs=4, use_worker_state=True) as pool: results = pool.map(task, range(100)) .. important:: The worker state is passed on as the third argument, after the worker ID and shared objects (when enabled), to the provided function. Instead of passing the flag to the :obj:`mpire.WorkerPool` constructor you can also make use of :meth:`mpire.WorkerPool.set_use_worker_state`: .. code-block:: python with WorkerPool(n_jobs=4) as pool: pool.set_use_worker_state() pool.map(task, range(100)) Combining worker state with worker_init and worker_exit ------------------------------------------------------- The worker state can be combined with the ``worker_init`` and ``worker_exit`` parameters of each ``map`` function, leading to some really useful capabilities: .. code-block:: python import numpy as np import pickle def load_big_model(worker_state): # Load a model which takes up a lot of memory with open('./a_really_big_model.p3', 'rb') as f: worker_state['model'] = pickle.load(f) def model_predict(worker_state, x): # Predict return worker_state['model'].predict(x) with WorkerPool(n_jobs=4, use_worker_state=True) as pool: # Let the model predict data = np.array([[...]]) results = pool.map(model_predict, data, worker_init=load_big_model) More information about the ``worker_init`` and ``worker_exit`` parameters can be found at :ref:`worker_init_exit`. Combining worker state with keep_alive -------------------------------------- By default, workers are restarted each time a ``map`` function is executed. As described in :ref:`keep_alive` this can be circumvented by using ``keep_alive=True``. This also ensures worker state is kept across consecutive ``map`` calls: .. code-block:: python with WorkerPool(n_jobs=4, use_worker_state=True, keep_alive=True) as pool: # Let the model predict data = np.array([[...]]) results = pool.map(model_predict, data, worker_init=load_big_model) # Predict some more more_data = np.array([[...]]) more_results = pool.map(model_predict, more_data) In this example we don't need to supply the ``worker_init`` function to the second ``map`` call, as the workers will be reused. When ``worker_lifespan`` is set, though, this rule doesn't apply. mpire-2.10.2/images/000077500000000000000000000000001461637447300141745ustar00rootroot00000000000000mpire-2.10.2/images/benchmarks_averaged.png000066400000000000000000003104501461637447300206600ustar00rootroot00000000000000PNG  IHDR 39tEXtSoftwareMatplotlib version3.4.2, https://matplotlib.org/+X pHYsaa?iIDATxw|[9G۲xd2@0BQZhK7mnv/h{[tpBpYcNmClvX$̏$+~?ǰm0M.,LkX0``Z#Fi 5,LkX0``Z#Fi 5,p@;w[oUsUss^+u!dw쩧ܹsf͚l\{;wvڕR4D[7pΝ|.o^Zz嗵e˖lw 엶mۦ^xA_TPPիWg$cpp0%}/^Z`PV\k۶m?%Kcѯ~+ٶM"_O?] .GJ===i۝x≺Ks… Gvۘ?NEiʕZvm---ַ+WjѢEZl>Ϗ#O?o}[:ꨣtOضOgNI͛}N˗/… uq/W__߄>G ȄիWOgoQ/-Z4i'>ŋ_}Q]{?Os9袋ܬnAnF9Զ[l{:u-+Ԃ 4{lI.566: uuuP{{ R/0}L/}KKOO>ONөoQW\q~g뮻NCK^y :UVVx7yzi]zںu.bATAA">+ /TQQCW [NMMM7)I:TVVիWOju$>O."g7߬kF}{S.[L'f͚6m 7ܠKJVZ?^zկJ:mذAկt'g?;fW8m&%I  6hΜ9vء:3$IG}VZM7ݤŋK\{9IҊ+tꩧNy睧{GguVں`0믿^e666#JKKuu) 777E.qL!իWH˖-$N;Mwu .Hl.EQ=5k(77W˗/׮]R-X@>OO=Tf͚ $@ڶm[jٽޫy楅WksIWR}z!}ӟNM <#tyw^u-_\NS7o֚5k}?qs=Z`zzz͛۱b ~5k^|E]Vyyyxiꫯ֥^/~ouQz'|Gzjjju˲,\rrV~;]oVXիWK|eoꫯVNN.2]zi};nI?eY*++Oz>oNNn]{S:ꨣTZZ;]ij :C?I'1N~_ꓟ>ܹsu1T{{^ΝZd!0v6ƙo=0``Z#Fi 5,Lkl/m[2541x7ox 0LTP#˚)0``Z#Fi 5,LkX0``Z#Fi 5,LkX0``Z#Fi 5,LkX0``Z#Fi٩O<2&DueO] p޺z).`\X8wUL[CM.`\X8òUF,0=`A/3??eˑ}g)_׀bDKX$vc`At7u=.` ,Hu q%B0 `A䭯WhfxKHCIXv8HkKKHCIF׫hggKHvL[ LL/H1LSv"2`!嗴sU|h(ۥ`!YT,;RxlBWYLWC] @ R ӔNMM. i0mHVnBMMY D"p(mN=wUSVn#I B,WwTuM7i``@կ'IK.DѡzJ?u9H:(+; ނ$i{sO*/9$%>5GQG $iժUJ$z'/K^=/777mOMy}. jkI` +yS}*jjjR]]]ڲ@ b5Syy9ƍ߯K?.LߨS,O[[Y) cAukV_~N?tIeY7+W}*ò̴͉ϕai| mjV۹O#`zL'/XTNٶ}kڼy~ӟXk׮~TvI6;[N؊r,IRx|5>^KǓ߶z=a`:} k)N(}*몯OczcqFY9s-?nf }gf,"'}+ukOj[B9+~[O#`zL'/҈}j2qkתwJxk֬iZ|UVV*7H[ꫯW[{H$h*?ɞ>`ѥ^{Lr~Wiiij/X'|rq }߮'xBW_} /]gUĵc ,hh/Wx7+ ,ҥ^:suWmH$S~ztAk|F?J]r%S}7i%2MCm#^yE~Ewbe`eضmg}U<Ю]o4pQW[W;4LݎuwK3*3U[N0yx=ޗɱ r&>5 Jyr(4BXx:?{Z2OmBMY 52ol񘘑 &#`q)Q[sf/$|OYXxGʪi# ÐmJD"Y Wޯh$ZkobU`B#sT\+1:KJ4,VG;T\+4RX:65f*?!»b^~YGz^y޺Uh4=UF`zU3d#»VVPŢq9yrg,` ޵ʠ [;uSWpsp2,Lʠ:%`fQG(14 *DVG[$U^O}F,WUXT%~9ښ{R mޜ> 4 W58I-۵.Y 0NB-Ϭmo}b]]ڕŪ 0 ٶx s,I٧XW`! Q'ԩ la>U}=0%.,K X1 S"k-5(I)١X_ tXahҸK䩫$Y`aJXSjn(,RΓ<˕X2UAmX.۶e VvI`,Lʀ# Kڵ.ىD+LYePsʶmIRCEڶg20`ax}NJ5a(ԘtF)54նM+Z^\\N(IJVygf&0=`A'4ίDqYngᐻzBR ^BIҕ}Bw=){/=ݬ$yj"X6į,6rmvu$o]f&}X$WO*jkmΙ.ᰲV IUT8Wk@j( *sP`Pyǟ ˗z@IRMy,PKҊ CjkINjܨZ=`z $r_8V*Z Ne+TTu5zuwىDjGۑtȲ*)HLS[ƬNkC1Z;kYg[tͯȶmyVkWVkA1*ާh,ʫ ԽkHzf(ۗz@`a [[VCiE@!5ȑo}WnR3Jr:L5ۡ$km[@gڶ5L-rXm\43?IU5pȎŲZzl*$u4znAj*)S?{gʎDnigfMSX87G%hnL[;ѝOlV`$K% E1gJPSSVS *9[-P,-7'KU%깵[e\rWPhS+XOҲJ[VRSKRQ_m!^=²*|b20T_ʀzIXB8Oәzc$Io<6Tڲ6)TT^PX+۶婭ShSl;{"$)Jé֦kܢ?Y:_?!yPtW#@AIe93UX^" cYL 5V[s<5uPSӛXH}q*H+%mޛ$=N+WׯYL-,D#!5S }jl^%IUA${qyjd\YL,؉}eaE2,&2A mjR/D4ݢ BS\Җ⌃tQ5)jXBr{ah޺55_|E=Sڹs>FCCCjjjRMMrrr2yzEuhi|0er,T8?W͐t*(oNFF`E"]veЇ>_۷'Oh>/85ޥZlSxh l QБʤ/{d8rϬQil? ~_衇ҷ-YFmֹnzꩺ3qjKUT=0ŢԲSެ7r E+mihSSVS##SN:5f}}}֬YS] /",XeUAb u+x˶mF;;wjܹ,KP($ 9}z^]EP[ųTT\0+OL.= @Fr55Mяjɒ%Z|~(}կ~UGy-ZUV;xGuϊ*ꔈԵ9lfi~}q*g2i13*IROAzHOz饗tNa?zzz֦?^Gq{zztF^{HPHW]u^ݱc;<~ru /pIE$4dzϩ޺z_ fFX0 d$r\;Cs"Ν/~:3QrM7i``@կ'IK.D{WLdY$騣zGogZ ˪ٲIZ: -Ǘe6۶STRP~]"۷]Y@fe$SrEn䞝B㉬׉9&'t09x-de˖iÆ ~;Nk׮Uooojٚ5kd/_>~3g֮]|ڵx9m/\<o[G>|Iq_3'dZNuS}yy5V;JΩ,sCz7sGu 55P?dd ayy;<'?5\#-L s= ?Ywu饗*''G{.xڲON\=3GCMa?2`o[^X_qvi:&AM`FZ,Ir8]MaHj+zV c3+Sejkբ)EYd^F҇nI~ߌy2--K{I OuGU\nX_-i^TS0%UI)'G^~t)jŊWJf~1ԩ@on!u[e>]|\Li{QҊ\ŢqjoZȌ$L+Vг>C#*v{QXi&-2 C/T~{jό<{NKZle&02`]vejllԷ-[NvRww0=*)LnNFXTQΖMiWk׭dPL];婫*ܜzHSO=U۽8=&P}^j4^UW\ Jv[t̢ yv/X(YBUzHu饗X}ÜT>ءp#*#_Wn~Ij$'VYUPֵ+frϨV3kH}.EOQ.$9ȫr}.{ګ#MUcWdҎ}2Ye!I Vi? ݛo_}]ڲ[{8'^e/\2\W g>_WoaK'NӡLmn$5t5O$jnxI>y|ɩ/U)u40T93OpLm޷U񱏩รZ<`}򓟔"GίKXm;S5VٺIUI*ҒYE)vS:Haȶm%"a 55IX7&%Z~^oczK"y{ڤCKaUN@^:[R$5aY;u0 % [z 5nZ-`ޠ^xOjr4¢:u6-uS[։$|ZyuujjXի'\]wi85&ZГ7i9UNkNv{fڲ4 jk^~ojM`d$z/x\adx [+.PiҦnhW_dI{aLWէ_WPgOH=)yXmxambv}EPړ:2ް#B;d7TcRK+׿ /_ڶ.hNw0LyM5B9 K1;.Ijn!% S۶nZ'rhƜ%[+-ʪζ>n Tx[xW&-Zp>˶m/_m ÐՂ t)L֩1 UIsefؓyЕN /<,N0L/H6JNw#?JSވ/~S5jU~ Y~KФX?xIА?|-^x)`bTom ʑ ^}nڡ@aϔm (ifYc@ңm=J&V~rj v\Y..ALϤ HWUBڲ=#} g0 ujhѕ}BSVO(+;$^١ ;kHjTEYT_O(=HN >Mkz!yxoXo- ޡhs=)1i\keP\V ]ZR|$h3WTgfi CjlQUI%1Ś֣hKK(:9+s;z½!=TbJ*Pŗ?)ᐝH(!bq[mZH [ϩ$ IR$<憗TYPno7V}E@ =ܶm+Rtk[{5$i(ɻR!3g0 V:AVhӟ+gWx9LODJSaqE:eF%[R&ϢRzyGȭ[V#4̃åNݶekcHX$gu4oL-o(h,gvw^!3jYyQ0ڭ2^ {:S]9˟gV˒'Jl_Y)xB=Oɍ Ce(c@5f >-yިGj6Ew)c@v<@,~27VN* ݍZ\@͗/7_Mle:fa ]Ў%kgWgu5PrSA1waKaݪj]z>x ˔pn;Jr8VR2C1*w)Vl{aIO(5<-u*qD+c֬YzJ;w?y͝;W}}}z't衇(Sǻ\SM->XEulYfɰ)a2$||"-3kN]3UG7in8*%R"(EwvYE0dxwRαթv4DD˒ +ܣDTJ[-W]"M]4Tcj"`W'/ihhH^x${:,]q8=&9 *SsËE#r8=rs_:F/^ŶwBL%gI3 VmٸKǜTQF3LS2MEwo?t ~[A6 %+ߛYTNز2-ep[RtKH\<7S4Tޟ,'+ ɒkQCC:͟?_G}tjeYZr~ainN^򠤑>X̘RVשgm CJt$Ӑ/;\P%n,(Tg+>8_ 6!JvV n'1)ߗ-D_XAQi*d6I ;pݲr]Jsd`ߕkU|WHd8(*0'/O F5fyY9x⻆~CErbR"S+$+ae0d߯.H˗/WWWט555?Scg4T SV\1IQB~uPT=!t ȳT\ΩC4G/[^ʳ` V/+18}BcuFOfKgnC++ϣxwH%vIq[SeZCm=)#"/WM0E6wkp>ahB%Z>ic ;eco{@D]rͣuZ5oOeSz__&\ajyṋҦޭ=Sl m)kgBv$סBTSK<&;E]:%6O,c7*ӝўXSŰLYFo g۶졘 p-+X7XKPLK=\ >ќ \̠[νlLq =,3-I2s [c{eW3){p/vRKon9}FB##Vuu^} ?㪯ĩ1fե5?Dam p9g*'ګ#ert@De*4'p,^,R"4$;aa ȇ0WM\5yv4.;e8,9Jr +'p>#ʶm HMM4s]S>'cPv8.!eJJNvǎ%mUb "3%@o턭!9N.+9bu0"gU ك4ƼѸ⻆dr\E7_I?BtR/,ƛN;#_J.dbdov-jDg_YY;?2`{kl2y䑒E"׿֣>|;85&LY4~Cg+=|9PU#QUiӺ{xC"̀[ۥhBCYXXB/)'I2n*kNAZ@e ޡHc=aYWޥr n+G7:9$a93OKFŻC C񝃲#q^yHj>Eةx_X%sf^걊u *qdHfqjQSX[mM#IfK9Δ$߿I'=RV#5E9r/P7#5Ӽ'doQ] d=nWVG" ;%rW(0ړl?jU#+׭xoX]C} C2<9JrzxO(D~|N^H\hcdXfjb0zRGD|q0>'+3V{rV(}c[˗oY D8h-2SC¯wsH$Ӑ %wޏ9Z9Jrd8L%zECkYOMnr9gr7wdU4^nKdd8{e]򭨑u(N =,;gaB*vMknWHdOD_DBoWx].)CJ lLKs e-%6Ɏe8LEk&9*F2nIX"vɽD#[v<к=2s݊*KSgmmVl[~~n8Gѭ=2.,8ٶ-0Y&(]xO8wuyY~}dJ2<je۶B/ /Wj!e2=_T?m_W]\ 1'fSmZHe鬳=ܣ-[(HZV EZzԭ^كQ>pLS'-SnUm9]nTIKzúuqu-^#ZR._SVezxe⮚_Z~vrM^3a[R2q`h\QD<`Kod_mzc/pb$ۖ->(.KV/zclz,135"BF>T&TvtN<5:k$u (1M;2 W lܕZ.[r YL~|}dz׼dhZCw YnZ4TK:G_XBis8o гkM[YZ!ςbE[4uVW#I(++ߣ'[uԢm1p82s =?yߐ=M[F^١Ȇ3ZxoXm3PْǷ)N[YTG]d|,,' .0~CpCQWl砢{FPÐ{vaJvԺ;] e)ڗ3* r*֟8$<)?!>; octY# 396W;VO_{A PWy{HG%)Qx]܋K]2QPmo(%"d9N‘/< KS"d8sRm(i4 m%Bi#zoy]]+WmQ|d/-Wߝ o1Y ˔dJӶa8+2s'U蕫6_B/lNm+ZI/|]q6ޥrVްS緧Q=q}lˣ4LKjn#*d/VcPmK.4$>jsf?4ᾜv,48gGI>!eRkK.ҥK3y Kv?XDF3zwaxU[P Z,[jx\ιǪt wujn54LKjfZwZt~^ۺJJ4]u&0"Vb4Z6LCEU/)uVW&oyF>py|o* y˳`5+׭/pf| s..8vOu ;_ͷReiwv_5+uLVZ_k҂d5PQV-ﲪQᡝzӖ$Nw6+gm^qᩨÔ4gTR2|ڽ~qJ*dk-{( P +1*J~xO D=랝 &ïvݏ: ,Emy%yQSkHnhfK > >Uv8")f,Qx}"ws-*{kN39QrN:N^hKufP9+jdF.,C6*>{tܳ nة'QZ3s9~h\(ԥە ^ﱖ>a&ArP2(M&iFm+җnh'T aT~1 iB"IJfHQjmW2*@Ck)1 G_MݒӔk##%)[VWdHg ٰKgjsKv)'Gen*\L ,w?{|fKQ}MCbC#n;r %b =ٜvݯ9F H;=yC1LKǡ@dL0crVN؊);}= e,v*s(-,rYTl@rmˎ,򻔳F{15定%ٰǗ{x;տ1mq(x^ CO(џ><.`u (ZGzcH͗l;ePGZ>߿ޑϱţiiwcF]J)fKrH.p)83ZMYX"l+/m$92`a3LCE+jrHv=pv^oÒ.P9X->̴m\5H1wm+ ×!pH7V}yU2n9T@ҨYiCXAk1as(?܎YqLଜ8vdߚL&ϙ+gW4}NMZz8|vVDŽ򯜸d Oo11#A*\/X6MYFLn Ϣґ VУ CVKV-Ir%G=ӌ=VLպgYX"{Vluu''Ƕsc YgQlPK2ݎ䷝3sI߁=_{ W\6_!nj@n%HrJ{w<:D=wmbi{WmʬZ1S}a~W*HO;$I]o&7GO^סȆ2=rV[ )ҫ6p[r_j1-c>1\tfÌ{1njwF]HԷz]Ci#}ΔkfP;4B[VsLxcdI||b;GwÐgq\5ynU襶ڬ||Gϐm꿧1}T$12}NP?ΐ˗&O]C >!^KnI}^Ųr݊lV} }{I\A% љްsd&`rp`r$qqH>Ί\Ѹ"{ҦHbmuTӟcMMgw[rdHGJkX}aĨ^ӓ"1H~'F~:~ NߙE_;O fdw &+;a'~C|T0c{\ ޑ6Hz|%9zԾC(+ag2|=* 3$mu8o%QUr)TOCO4Jsȕl[CmO fdr-LNLC2ݯ{H[FOww%P \ (KqL3lx?mHcM3fJ^³45}OfKMgsLaXf$gxgKFOÔcK3!#}sS#w;byH5+Z;{YLSgQc@і>Zz5V9i6Ώ{Tɣ4si#u K={hq0r_w?7~O"x`Sz/jL_i|d$G:+AjF6uens$4~}]˒ʱv`="Bڼ1MT?Y(1?dr;p$G!$X[d,+r} HG_]C1cz}ܳ R-ku4W->L^CHr8iӨY`x#;Hص:=G3[~{#N9.c'YP,M0-Q <:’oXTH0q 7n$='?aw2{ܑ zX KFd9YY䍞F̴ѨT] GYvsf-twM/(NuyEiuwj8-9gG71#aLjpdzvTijM{ ~wq"Pf=n9g.SHlc{Ð#UrhHcry;(pĿ{q,ϕ|}9d8 X't|Ao駟>9]qZbz)]r%~(`v~]*ע(njE+Tgȷ!ٳgGv,ԃwn]-:|7)Rŧ/U?;=S\1@b49g(&Eb\\s ٰKIE% ޖyUu1 CFSJ|7H}1Iv$ +L >jָɎ._nU,)a)ҰKԷκ|^١';nfj^"p[|E9突5jPb *;U;$m&.c;ӷv?~b`˭N[*q¯w&»1mkZ=5sgpZ{o^:gR RG>&3'G=>p0$'’8ߞc}Qt;?m¯Px]B풆T䦏88F[PrĨ=9VkWCj_34;-r/*U+;b2|N嬨ID6OC ڡ..39©'# [c5vzo~-yxB񮐼GVɚ]{bܬ'(C]!yM>϶*%GsDnw=|%ڡgZ QO#ܛ졶CxAݞUr)vEm镳*0q07e-[LD0Ԗ-[R5444Ⱦ`zFt) HhWH!u.mmӪ#ߤ4a:^xBi{נrY1iˎDd8B)2ix Mou{DŻ:;F#/Z^/W}ݜiW4 Cc/HN Pm{`ߒw3fny''}АnVUU ݱc < ӫ mK^|cw !*qϵr:ٚ iZyD#2'Wʳ5ɩn%m;W}D5MhoWK0R}p&弖9!oawÐs57s{iFIM5܌z #+:{eO|)9αUL';1报oRWLX]v ZJgu(l٢o]O*I;t衇fLyukK_B3u. %+ GjPUɛ7~N,ԉӚ[^\(fӥ3R>iNW{u3=#qE(qr XVOSWںٳg몫 ' )y?O 'nȇav^dM=[4p܇))\PSaH=\%I3tKtֿͯ?P\)$p2=^moTťy~>,[cG|Y&F9Nʳ4˕ɐ+VЊ+cJ***TR>prKM`ZkCwΑ$zI=vM:+Nnb[{u}-,뜋=|Mpl/ZW m"ߜST%`s/WU{DE7O ~$-KJJƄV7>Uܟ $7v7Kf*;IN:J .o<:aڧ >\SݏN$Fg`;|[[^{5rguV&O _ 6nS8rɕ_?-5L]\j'oo_q:n|mMOIշɵrnd$ W{WDBaC =$W`qb5"O*H]*/`QPA=fZz &9V}O?m?ܕUnޜg?}ݧ/~_*۶Huqi޼y3qjdج:.՘xl %&ٱ ~=N:4tɳZ==f# %YX_rjqHu=sѧ>)͚5KTZZ>Zn!F8}n7U6sNwd8\2 Cu5fԌ0 CUN+z:AU}rWVᇦF7 vܩEI<$ihh(~ʕ2qjLyu7nS$$9^̐i9߹MՕ;n}ټe:Y2 C[vih0:v/G_w^t$)?e_HUTT.IU0ԦMR3qjLv\zno]~ =|s?UGO(f\,Cwo?zte8mצ}Y==:UoHh"='p:qtkɒ%85`mNHH_{Z!g)Ӯځ4:Opl˜3?Xv?:&YR#Tש{Jm ."UUU)IN-\}+_ѕW^\}_ĩ1U5VPw4!ǜ2^^/;Oե(uJ:CZua*TpǍnj%A.]Knֆ dpdԘ":&IһUxT.˩`an:[T|5B/ܩ]('Ug/ǥ3?X߿Q<sΕ랻w{M]&}А.2q'2M͛7Os!ܻEdk$3'_?g?>h,reiŪukp W_hpU?#D8,;N`2LzzvZBgn$ۣ֨i8I8Y9D3fVHĴN=rOqe۶Zj͵J O2 /CcuUS=`TRɌ٩ە>}+765fAK*tZ\ubq3 C'?W 몫s=jkk)0 ΫKwhMʙ_M:/8B+^wꮛ_PUW|YmjNJNqLo MN8A|=д;LShv>X6nM޲Ym~Iɫ5hb:vN{"9TaLwlRsNaL馾rʽ~XaLTUTY憗RN8֬S^":pWU^$6ͬ+PA+2gPͷ/$Ňdy'!?я2qXL3W|%j!ipU\Q =.re=#_ʳ#a]_U]ycrs%I}=*x<3kPL!ācV>X[M$IE$uSjVo=yWtZ:%**_7n;OB5_c nxc `XڪJ+WgICڵk+jMĴw$ɓ#NQN`$W:=S{o{M[wkƗ"Ҹp HqF}ٺUUU~bɑ9z-Wv)cPa6)mkoPwǔ9XS'o=y*gO wB6ىV ꫯVnn]}ղm;m;F sU+NnnL jk( K+ G,?V7Sz 0tar8Lu{ LKTU_wD(jȮX<>`ܫVTT=F5gb}zԚj͒e2 #$Wlӳ5dikH]w:E±1 ˒+>8-߾J;\=&`˶my< ڵK.+FNՖfIRT.OiC?(\G,g~^NB_@dLWk?oQ pHuAw],ӝwީŋgȂW"FŠT%Iy^!0y%)uY,@Dwݼnp0 LY?XBFO}SzGjhHN۹s֮]}cjjjҧ>LY[={qevn߬pڣ b*dxJdut )* ѱwn'R}O=SW YA?x?~F_e۶~~ĩ%cSԳYD\idܾY^.6mӦ^-)߿&גZ|ZE< y'tT3 nwr/#/Ov"!;ɴ\~*#$uY:SvZmޜS]]c9F~?SEʫOK"5.8S3u̙LmWVPSKpy?BV1Xl݅iN؊iŪl6YT<g @ƽBB3$Ю{2ȨL!~P(|Pw}nF]iUVi85hV^nFGEjU&Ζ&U;L>5.y)y_Q>#(Ij\ߡm֪,יX3u$)_,IxOgqj}+_Ӗ-[ԘB|z RG*1[h8u{ön=NblҊξhⱄn lw;gI3f?WsNqLXCCC;ue裏}K.Ga5voJZzyZv*ާh,畣l5)ʎE}L^O!Ƚ 2nU^,9Tkȡ|9Yw.#֑GP(}iŋgTffը}͜w>4(0;&2er.8Q ʵ4tE14q1#m4Ur*8|9JD2x2`s9Zj.]c+Г.I>X'\!I 5azqu-8g˽«T3Pkh@_DGXP!gaDB\*>[>##7LYyu${≸,RQE^{%qٻ8s);W+z{7)c0%!$B 9N ›$z'4ӻ ܛ\dwiǬFZKr}>Ņwi-o~8yv4z7H$.s E9Z23iN IAAAwx:z{{m4=k֬ѼpdVv*$#V*]+%IVdqچ0 O݆u78LQ˭“in衰?d%(N=z8DAִ   1J:vw}7Hd6m4`m몣WBV^ ټR"&z$Za%IeDV S8bUPP˭aH~wldNU/8Dw+   ͨ4;̲e˸[0 o}[x㍌;Z?ƥ@Io=iRT˾#& k'ãQ3ԊYyD߾CG.YQʽ5t8w0z   qJsiq7rq0a.B>$I⭷K Iѽ0'ӓ_Ua;us/Fo'ѻP͸lxC_c[N$ѻj%.AAAÍJܹsRubxٲe<裣qi0Q㯲N}:[_~D K* }lEׇHUcsrnCy|_>7>; =kj:w)   nT,O(vxOۧgic‘o>XS Zim0-32P􃧰iSNC,v1advʙM*-뛇}|Jd[~BdCp   0Q ƏϺus;d͚5^;v4.-&Y]f`pyfְ |Ұ/QOAu=TUf9i8$I+<$ȲeeQi?EP(   VF% bִo|ps 7$Ua胕]XI۾:k,C)0zۉzQN%TU&s_ᥧL7W^ x $   acTO|ݍ6l[^%^ C96.ff~hX2m\q:Q32HogXlAAAA8F%夓N А>XiEUXzٷPmv* 3hl |xŏmHyD߾1b `gӇ7d ѻb!O"/B,$^i.AAA$,aH6p>X6bKf/,NJ]G Բi/{誙d帉Ǔ!dK.#묳i{! ѝ   :` j4`"DcH_;=D$jKDi$IH %L%ּZ˞qI>{97{aa`pAAAQ#,aTdV=ޚF]TI2լʹu;yc،yeylY4d\+(ioC   h0r4{fn1`>$ ]7H$?zu}0}`odN=o"c&“[x=笪"޻4+h ߥ   p0$IJFQTYV&旯zsG> M:Ϡu|e%aƂ2x͠$};{߭$|   ±H='9$c#I+V8s5iy`眂,+8*n =̝ϭM=W~ύK$Ibq K@7ot r.Pݮ   p8 ٳXׯg۶mTWWSQQΝ;پ};555L8@\Z8յBO>ҶW>J$͉/ rHD8G6'`wجqGE%oԬ, ]GEA   0:Hu7=^b+V+K{׹k׿~ .-\9x5>wֱxam+dS\Eea+7O$G>ZlF2K̋&? '9q-/Ȟ4|EvAAAبLW/^,X._Wqi04\,0$ͫ, }Jz#u_OM#K]kl GE;w)   F%ڽ{7~qϞ=ïr&X} M!Y{va%mУ4/|>!g}^dܴ@fOO#(6d\Sr {mxRAAA8JUZZC=Dpx)))K >X:w]TIw{HEOt~IϽƗ986z"ږ6:4!SAAAh4* Zqi|r2]L! ~6Λ{XxR5fvz-dMAAAfTN:??}ظqO~q7S}ֶ`[ϡz"N"(Ǿ}ֹD:I-p@`dR2wI hn?'yɇAAA#ݨ-ٶpB.\Hkk+ 3ZsՙVoP+\fpOUic>{hYzvY6}'E^\L=w  ,; AAA_NNյ|w.Hݛ)LNGX$"-b Q?fLUۣ LID\=z0Hŗ)   '2j&8S={6Vl(V*;>Xut4e$QU}Y/[,oCI6o+ KUmaΛ{7o:Lr?^|wAAAtR}v.2t]gٳD"@VVk֬! ӟt4./dI_ad敠(6lA9|`.?EW`تE#B\q֯w9Id^[qW`/5s0 Pݦ   p [o3<í: ŋYfh\Z8 F%Ԋd`Vdy)*YE8O:/##^zcˣSPUG}=i㎲r$Ig[g$Ct   ‘hTUVq%5lBGa&2.3.a'a`wM(*lAynH\ppS\oE&ՅAAA#?~ɔ)SFa{ma䕎%t_U{FDR5V# @cz^V N)L[ @nvw1}~z:J(t>0 o F,Yly,"vG"٨HXMRAAX2*//~qopwPWW 705UV5F+Dڵ4]DYtJ r9=qA {? NDG;6$oj󤒄$e-!Bhz3 C|AAA1*ŋ馛O}u]ax<nf͚5}r]9ôi" YFC{?S_ {8LUݮS[c,={62d?IpAj_#n[v<wr?`CZJavL+ܲAAA8Zi9'o]uR.\s`W,E|\P\n[g (` r|Q%#R-j8?$-bk \ ]GGmQvr>Ngos'|sȚ__0d;k63HtuVG;jF{]uLϤI-3 PCw   Ψr\tIy $K2U ֵm ,0)q8k~!GIG$I0oD]mIHX( pSG)%@W'?0H(2a"v_ = W_wеAShE%8++\_AAA]`xdggsM7Q9̛+VpM7Ϗ#`uEi w vyw[',>dӽ:j+ y/@$* g,ܣ9Zӌ]$TQ^am9Bz$L@q:Q~b-7m ݍ{4^vVV5x$UEǐm"AAA8PF}hnn .[nR hhhK G̡}r\rhl1&\PܦEq,ڄ w`۷}$=]v90c3kGUn8(;؋Ǯqqo=6 H| I`C'g7kjVST-^RB#DĔTAAAhꫯկ~kv.%=8UlY;VUSe9ͫn_!'D$؇$.?5Mm43DG[dR_Xaۣ;e8wB.FwgYp{֊(}C`9iaX{YgExp>u 3z}IU=m:'YgVuef!BMAAp7j|>wq~;oٸq#v^w.)AdI_M d @דx6r3haC|Ԃ֟cS=N_3uL6~ Z)  ޟBT[n.$ѻf5Y8    W&Ou]>ڗJ+v$ğnfbjg@UC|̱sIJˉ8oM;mɢ' ~ K?ni5Cfeƫn)x3Ue1wI%ngG;^_4--=옽dMMLtGcslnMWY]CpzbΛAAA8Eկ~ /E 1:w]8 wF:䰭G%c=;% wa: yX[ (O=;8ҩL$IwQCӛJRU᧣V|+.fooǖma[|X^|)&:yZM ee%  :h VJJJ{xGEU@ UǼYUcݛ\f;hd0fds`D e4JNӔv>u\z`+L0&BA$ <>O"֦^:C|Nnۡ[SQRTg͘8\>+ ٵUAyrrV~k$s[;k^m22*¡8[7p߁f?t%IBzQ^vgee7ooGTyM#Bx&]]`h/[P؋K0t$I| !  ¨wZg%Ke#P :";Mf'6ų'{dsDJ+'taw ĥք(LF̡gW}#~aVD|7_o[|~ !Ov8Y%IW[DxgLszdD{;}kItX~ﭥ4#22- ݍ"ɇ9  p; #<g$Is9 G1cnL8=fLewIjjtVWiAre C|GU>>ͅCqr+KX[709+cs+mA|nvȫ$UE;cCA7;3oo'FxVd,ZS#*jVʹ"XS(2,$U  pd8 !I~:}C$IXŞBH2 X.ڧ(gW%sd>bC|8.C$ #$I.F~/m«f5=;?ˬjoc{qkiJ iH[kza;m8,CJ'ӇO(S="r]6nIBQd~&)SmlHAAA80?ii(B[ijoŪg~3kaژCvKD^,?Է%Yjً*x,i0 4ff$Y9n.j&a͸ݚpB,|Ti#} b-fa(̀wi־ۍg ?s%F"A +R+*=Wk/؋J<ç Z[d[ pLAAl$*OeǂAjV#҉奻: +'R!M|șrFΥ_!ٴ[yܗI MSq3HAQ 4êh$AOWdœLDqZ{펠O}yU3lNMLUor=8zk0[Ni'>ݪ܊$zi{!X_4~y;ѹY ]'s)$ ^uc ^EqЯwC^tM0  QF4~}:똝?G[N&?W~G*%ۆjNbkG X҃ѹQᐐ$ Ӗ>̙M@ B}Qz"Rk%I;BÞ.fȳ&L+d׶6_/Uě +5ס&) Κ1icտ=z_TBNUnA{u lƎ7Htwc/);s`&}h(Ρ?*=ATEiaF$qyW-BS4bI: :kp4ݦVӤI|f%޹o}mhNE}x/Gz& Yx|e\P@"0զ`w6Qh$AYU_0h$#|׬o,wPZuWNܟ$I(^/׋;ìZ@R'gD>Nljo|ǒϡO㢫dEQÙz\\Iq@^4 7{lNAI]wu >x1|NJ+3³Õ޲o| Ju>oxgͦ _Bhoi]'MVG oٌv9o+/êg[].d]-30+fv"uuOoߎƏGӋ}}4/d~ʪ?@AA8뮻:a5J+j d4HBa|󸣡$܋ƟHtHF1'[Hx|j!0C0 8]N [E~ڛtiC7iOs=lVMt;/Πo}Xq1 q1#SO鏿'؀0)5=z$2l 2OZ dwz4 [WK/; ۶;:,:~ &ӃE羀kXZM f÷pO?Edt2}.F2gæH}ߜ ,AAa?G\;vkv9쳹k?otM,YxGU_.ӓ]u =,zl, rp% =I#rpt |lѷI>CyQF$\W',NO&z7FvGh k{;`2?-Ak N&2'ٔQ{F, ,mSs]U7' YNH+T&ÉdYUZ}z4}z^Gi ނ2+{vC)Sq֌AiNӟ@o+4=?* jFQ:{557#;8k:Le֯#;c&j0>QXf)8Dx%  5V__}}}F\|3_477s7DᆏSkk+oe+eb4FLwn\IO(]FIF}>Y@b;$b9#ޠpOa?muDtGP;=]V?$׭}~p$PTU=C%e)z4}PfXf#6 9 ~;ԛd2ɍ7W_M^^އ[oNc][]PQmƄyQYƒصc $I*f46ѷ͉6}dd9SGdn 3*z"mivƾRmRTˡ:@<$IjXͪSS HPCSZ~6Ii:SS۷Xαi@kZkl-9} hnoJ"uu_y|Xg#*/]a:mO}Y[xGw dojwJH&=?Ӊ#r(who(a/.!) KN'77a!SqW y]m[j*  p,\;_\qvz!ԧ>+0o<+8믿ιǯ^+VOo}c__]5UVn+MvQ%~fe\l^ 6z|K\j /0z[@c᧑U z$ٕ1dLq#MVHnTVwW^sJݝ<dE3+r ^j EΏ}`+L/#֊v;r0HotH0+өh讝d,9 9+y{pVkYxroOoB}JJJeGkзeDvlH$ȹT% O8ֹ9dlF"Ad|"w]ښ*ٻz7է gh{Axd'Ki_qӆفHfՔF2x<Þs0JI355b/. @QhA$@vI6ѽfoVKଳYΟLaR9KflP/AnUO<߿o B7zΛOt2MwĿo  GQ å^ f +z9+?y{zz|Cgdd}݄a⊏uf9P*&ۈ`ms hŘ wG9G<aLÎ>ñr$lykrD ?E_ܷ x`;8iobEB$ 0H[ENjrGH8t>;^_ΰc@E%Ï$+YY(*C/qi8k7l66՗Ap.]iϭ\ìb|w-yQ+3uz~oۃi${<6mk&) $CK{-A[^z8@DsKJɽӴ= {~?YsuiVxz׬b=eqOlX(`4v^زkm~%?l7{?Ц &mHI &{ P`y^Oy=N&kv6V{{;-V+0,>eBmN%뚷{'n杲IfKw].0 czO+"TV𪟒e;A*2bBέBH_ $22B1x=ɲ8`_#%6셅^:tʺwlfٞxSN"L_z\c?$k?#DRմRߧ["ݸ'L _5KC l8knXG[ҫi !K GFyuUR 8k\5[vѽ-2%q%'p{g5]=CꕔϏvUՔs&JFWWǗ jkG"hCj y.αf5R L/Ӊ4W =O wiX555l޼z'==aɡ+El +jmfWS#Ytv) غLװ8ܞ'놵"vddK),zn3<U{ #܍sWaK&uGtd5_UYIg l㨨Q1s^TDgN 3[[.lL aWs֌;ߧGhoAqK׮3y{D=o*uTfdgt\'۷ׄ.7vBדyhtJeieNAAWxzWL{ifVqL 7dxKyAaHx^Q G"j Dwɖ wvG"t #R#ZW}߼tnGr8PNƓw٧c1_-F4{cTpap #{ ҀQ ;Tf3f'^@Q,X@E_?̢E;za=Ȳ̂C+O]VU8o2v؏}/(`UٛekWsg)ɔEf:kaC|dsM:Ğw1QM;+m%lܻy@D|[$vVi~1ɧ yc?}ʶDW? { pj#f`?;}ưcΪjm0Lu/7#`(2_:/LK3vMYU& m@?-'d0Hl^|(+GDΘ!@覆ƩG"C3BM\c\z$lzx7nܐs|>\.sI8lJ]';$JZ껫M ;|ϖds` e#Ucj.JHH#g`s!z[^E)YS*~{Oa5>> k>ʱ>HڶRdzM#d=H&= {AY3G*IU^~u ^'lMssM)0Zy}[o!3dϡc TE$$M3^h׏x+?Oת#쩕ml<Ӧ cԌD^4AN?ojU 2O8m?(s"̖KIK]ֿ-֫LA`>t]w؞$~c+##;3G\s5n?|o fySYiXMzbdVޫLĩ(#9}D>QR:_@ D߾WP' ٗKmw9<$n 8O:/ <3A8:|XO}=߻oa; qjR^,dǖV^y>22C5x5Ol>Ip6_5_H ,d)5iS/ =w|+sJbKۃ;P<2-Kw~Tߏ:Fr#mU0{Ѩ9%25m3댳Ι Hd%Iv;z(D=5 x;Uٍ?^TL]wi!} _XS]/u]zph߬lIyQz2IGY6,4{# igM=#lMHԯ#w=j͂@ub˯7>m"bk{|[fSam 9G8uN)Sf mlB,$NL^\Όyetwٲޮ0;;..r ;S5u\m=D$'3ۍqp*mA\.3mW}avguMZuaekG+I}P謪寎xmZ0=m. 1 [n92)nd֭iS)L+[S|8kx^6]=q2yItwӻAcNd g1 !kAШtxG2e 't^W*$R}u`j2*&Aqh +7P0$IFpOo Y8Į Z+YE4drڨ]X f0=O=~°9)m3 @-–®DG;m? ܣC/o`Ϗo$Ҝh\yW~ט|k^`1GY9УQB7 c~bwT nT~>oy4N/l _)ۺ̞W[e٧\fS葉(9Dwydvw5CjJ3Ao݃V$vFZ}HD ? P[G߾df#u` ݡRXqE'׌86*fWN< K&q8_$ 2dŒ4롹ŧ+>:Ģ b.LQY&;wg'hζA&L+`b;;]6NNn$I4w;q  ޟ߂$H1+*H#Θ5l#DӜ/%ۛdj*HEom5SF#$LF_2bL n%ܔ$?댳pKhBץY]L+K*) ԨXN)S|0`5aeZnj>li=wyPrp_t3'(A$_0B]8OI0 H*&j#3$7ؙ2xLg^4a$:R*pi hXEx4/Ӝ&ٵX,A,$KL\tL^}v;AE81TdGMޱDWVPV=mc XͮP1&E Il:$O(.wv_oVkyTTH=AŬҚkj71IOHÞ$I7 ތWZxV:S`XHl99rrF<~MvbQ!;Lnk?&1mmj]AT4B -ivoC`G IQFlhcEK/ZCxiaF$i23у}psZfhZ3Zq|ۯfK yKsϤ0g8e$zz55L) ڨX]w_=W_}5w(|B4a}W*)$ :&$IcL .[:Uerj8-u`$Q "igF2AQ,yq/%޶KH@ {(2Sdok'S;90ǒ(ǝ\M(KcIbyl6F<;B, ??yu|.O?.l) SfS96֦^mlAc]S\n.¦)q"z0A$IBi6m1cG?3G/>ǙW +Y&+cn"{cas,GUqڴ?>Zr0 ]?ޠ`/P]C9m*VPBTI]| ܨXX 0xWxW$IhBؠ)63J޵mu8.I8qSX&9@rZm ЭF]aB|~'MtE\nO$!>χgOt<4pȬ GP|0jVMե57Q}T矣Үh yXS w&mv+yuX,-x<0W PVV6!(B2k[WSǜMq9G4d_[\Qb~ Ⱦ\f{<'Yckx܋T$b9} f`K(l ` f/K&ts; 'ٗN%>8r5B}Q¡8=]b5͟'[5n;kM^Ȣkhmѻ߳*4 N;߬~=$IkLf bcI+/aOe#GEM5V$=;זESF<7K{lՌKO3cf*3ï$͆kG Cֱ=K~O{_:|iy\U2 3c&_J'3}\$YГYHDd_F"1y=6~AÌJuOpۼ]> M({`XDH $ E5*$g!ɪJj$E$wrnՐ#ۑ9H"A8)I;T K#蔚XŬʭ+Mp3.ƬefW̜29xuɆ=]tig^4 κ5 y}*c'1DyMf5؜H$FjH2[ap`uiiz\hn6Ӏe㬋&[&(=<T&>v~jpTTHrrz$20-Ӏ!;=g–EΧMsϐ͛O+/iM5c>{9z*'|h\^8+`<ĺ.&%9v4>X@$l3ӶEWHHd{cAKAp&vj53evɈǞyb#aȊg| K2¯́O>;h`OXdRgJ~i'k|xjsx^V;n_Ee~f/ K՝V f*5E;  LhƉgKui_pS%셅 #>~#‹#mYYrs)^/?x_e${8lP/l0L%IP?B?&CnÖݵ̓O!cqDv*ԬY ܴ]Rv$ IhV8Ud!K2a3UljE$,9t0:$Y1"AE;>"a (2S|n܂Wv{\Yc]7jjBXZ@jzjSplĢIC1+?x,or+6K㙇7аۚi+LQDUmm}lؒ#f{|vis!+Sm&MهZ=f/*g˺f;\zl|~6FZ8]6Lea+ {Ǔr |,ܶVMۦUj&2wqŰG%) kg𪵩Y odZn.?FT9]ҿ\akZ kjL1Y ~e7{Q1Uo#*$Nq/>Xc<٭ 8ÿh cA8z+yfƍǬY>ATnŶ:έ:Uڰ@Q%zldgK6\$؆1½(Yh'~5L~?#"ȏP`u>/ݵ  8#r=Fط< >ME*L#'ߛG'έmX2W䣸<]7珫H4 >;ɚwӸ{KS(P\I_oV否)6<>b!I[s/HD,`[]RIYUh[c{X.ʪ8ni5]a][8Ӱ"ymv6dpG/̶^P0Ƶ N, ab[3i&d9|F_w`썲y]i ItȕWێ)x,{W7uZTv8p,7׸^U;׈Q}ψ"(F,j6d2[f&z4/Ixk؇aDH'Yj׈]6WBiw5ߜd~&Vz d͎ru9{ }D2un ;b* &xeK8 VH2Bva%^L˩K8kAF8$IFt ڤWP&$FtC %GtAA8$IfS\ ]bL6#S^MyM8I&y˦%F4dD# z{"ģI2\gs 8UvoUeZ?E[s_Zcx4IKc/6M!Mԇj0\)sRho*Οhzp[obbjuկfFf/*g"P0i}Qk-Ʉθ86:xrY@g[y`_ F<$ש񧏵ε4w%q5 }LUi$;/dV1x@,^ bzu^oҌ".> 'a'8|3GE%0y=S:>E#C+*T?УQl懧F"A?4E Z'k~Z<'h$M*wvPTg꜒a+I:iL]2nAdR0 t}RnUf Κ1Px925CX-/( k`/.~d:.X#'pι=0];Ӯ/;W^_DJv; ȿs Y1 d9"ޞLGbnJ)1(1jMܿo1gϟ?!3\Щ,}!q>I6;J٘4w/O唂>F2A1C-G^)#iN{ ư9]A IevC NY<ⱃJO+,`I#WSJ6f`m$ W\y_Hٌ$Omc U9<I$NʧnK!{cx|vI$rS8 ,>m X皱 =9Ģ lZ-]7hiEUe\nTwزs.j]`M,a5s9ڠʮX|}m+(mm}]!:ZCUeJsh82eÎ23)7(BtI[ֺ'LDTJ#Öm~tGc$*z,jc]/ ~]ڵr/Oo*Gs kΚ1}ޟdUd看\>V+vl9(Pٮ=>`A UW]Eqq1@$~ߍ兣CS-fg d'|#JUcr֑>v3L<؃5韞D)ňaD{[1"}hSN ~~XK ?D&WXp#Q˧n6C/͉$i 0W!_6t:<ш# <Ämz"cI6PU/C6Tw?ē<*d4%OsMkGf,]$uCx4l]ߌͮLl1qz!MaI&t7)b.@Ӿn^ʪXtJ .y$'c2 X4m:huhsnbƂ]!֭GqE&D }d}̦׮mbW}c:$(N'8O 9 /}e_jQ+ SYIhFnՠTR_?g7a KM }f5ì G$:ӪjpF%ںu+$Iv=dQ1!|5UVPDŽų( DumvdořVq,|?6HF%uD#ҋC.G-7U6.Hv7Ë 62( J=$KA>Po9bQhP%{PT_oYP?^TN'Xf03p{4ֿkmQ96NPUΨ奧pϟV6 qX,I8'Vt{5&N/dIvnij\^86F$6†4ϨřW;)}=lzh$ݡ|PVj ]$\R%3B% KVe;($#I4֓LL,K&u^zj+[7E&S96ΨM&^#; p538F(io7%~y_.5=ch ;RQP\@[`e%$U%Ϗx΂@U_H$p2QYE׿9(4 *'$C!38DHtwc$І t z$b}RI'ع w inAɩ12b?k {3rdQV- b'KbӔ!pfR~ͮoDyE>4–MTrfW錐p L$R=.P0ͼN$YV͡*ٹ5oa; tV<"3.D<0H$Zp' #dfٳ⌴26Ҵ@hVen۾7 羹9 ?-yd{t#~ju핯bۆfN<񹀹m`իb`V=|ZzT˥rl6}=Qk"KIE-} P,g4`v"8Y9nh? *?Uur/Ԉc9]BEm`qh RUVb]XԪcQ]M۪&Ӄ!kȮtje, A%8+77$M`/n'댳pVUڼikl-F"Ad.1M|ύ2J8"TEl{"./8>{c[',\c吪2 hj~?)9~&=R־wX(|K~&H4nJ[f՗jj$uVe`n| GL''-GNYCΨ'6߯Gks}=Q, (2K;y-LUk+L7FUm"S=άjCieVڵ"8 ]7[cwv֜fSŭZ>t`*_*(zڟjS0 H87td5RÅX\><}P~6Pbw,>߿>N?5=0OO+Vm(bone HĻ+sS(,[]RioO߮ =$Sģs6mh>;a9n3>-|k))$a; FqC^[atHd6MqqՎ{zG q3=#*ly\Flੑw355ҷj(Yq%EM?r/5w T}?{ҦKfv  oJUfT8f3e*޲nxܐ9Ss4.iʕK444PXXȒ%K={h^V8 9T%"vfSkָ`6w=~s |R"cٗIQ^[ = Z!WJx F # @i+H;(}z&P֔FAw<Ã݌$E^ ֬ 2*ds&;Nʧj\Yo)Λ{J)फ#ē#٤-}"Ar`m)$v{KPKFz IsYf p8s8mLSHĸ)O_ٰp߰;T]2noj%EQeJT1i* oê$3H|#ܰկ&+MVC7ػ)Ɓ_##*SRM-L]lU `N)dPDB>5 Zql }ԩoio"Ѹ;m>cvJ$;2p(N8'A[<:|%p lz YXtJ v0mN ?>rA<$,Ÿsp%Ig,(,!K;YXnz:c&W裹|~'U9$:[5#RxYuUim%I I1EshĢ }1~qۉx:6z$IԪb/y5֌FΞwm=d~f["7 =W^y%W^y%٩@_Ÿg~p׎兣T`ubbYjZ5K8bHu,X|\6q5ш䚿X(F_;znkL*A-au$.3rt /xJFk"g#{~AQh0I2_8e à'a8@hƂ24wA> y~nW%#ZpR5e5cǒ +ye~s[;=y'Tb)z { E1.8 ij/=T7F&L3W%_z\U6P_CMۿkX,1sطl@$#zZ^a+dB7nVIݚFK*]ao8N7(,0Oor|uޞ66Jk곏lLp3/_'ozoV^dey}vmj1/ &L+dZzymmc抖/=Z`? :̀]iӕ6良:@$g:EfDsݝcɴ /+Ǎӥ l˭aƐh $h9UbfP`=,_|;i]w*:"k_~.e^] !{~u`,dža@bIk)6lyoS)k3cq6ɽ ?TJ74(^$I ‘E$aW)p{zo??h6~YKŘ촩m%Y,={oa`V'MSŒAW b<8hBͮR96ͮ7Ơj&uVfp_ݡF*f86<>;mM}ÎK$덚q 3LӦso}VM'U;SjF;1 iU+̎?} nB3Ǭ/,seS҂KNaM)-.ᲙLrr=9_h$1:{{aVWcNk0jwL{}N>g;;,ac0 ߿>-4d'Vrklz֦L3 cs]%N5uzV-;&Y!,K8\6;ɄN8nW=ҌJ#N0yd|Ѹps` eSlCGn* |lyR?$nY-:⾎ylE˭IMmDWuٝ$I(9]r5ˆsPo n@Z2.:fcO:kSf0ez|[YC~g_:dRO W31RZh?-TVu>(SRc'QX6ֹ=%ia]$:X~S'~=>;tyoe}N+bL#n;I3X>s->kǢQ Yr%\rɰV"??4.-j2+wLl,_t"2KA8 $I͉ L}}9hNqD4p^JBVw7" [v~/#E~vs۠^j\dWzR9E% dYڨ_f9%0BUqݡMs8m,]6] r1aZ!.ƻ+yuUw|xrZ)( &⢫fPTnN{驭^T$whm6BfWiyo^ziG7߄j'cF$Ii֬L)>r |Î#9++__0ⱟ\3ز* nRO+@Oy.θp;VJu9_W\AYY$k.N~iկƥ\n((sc-aVz9lA앢/Zш%P.ց~_Alͣ76dSDn"Ӄ4z\~0HAcSx~RQ2ڶ1T&6kˣK6Ma%ٻZtژ>'3bicx<|Z$  i?W?X7AUm61qzk1wI%nƦؽisK~^`p uvJie5Q/~z>~d|uݜ׻|r/ƥ\U~}:wPSTɖ5/b:: ||nSXA8H=~ȶZgVNof0{X;3m@X&*7*(|\q+g,**bѢE֎O>[H}=.!l(2[ p0"+r}×+zl$zt~#c_FI5N6n!ymN1-uEZ%y8\@nJj,-*Aa"MjZ~yN z8cI$NmH[ θpR)mT'kkkEX%p5J+ӻ!'ìܨ*Hj( )6$eSb[>1 hF$M`%WI'_~F'|3ZXK\~eS1ba{*n pQ,}$wr[jܚK#,qdVB@V.M]DA* 'ԃ)AHà'}Y#@OhJVy>OrF>F IP˦w#MfD^F"6Q-$[%jGRĿ _d_|,s~=묑$Ip]V9[@) jHCOZF2ӂZgUg}2tz7IE8_Ic?1C-ECRmH ײuFI5Ԛ(Y$vlݙ: $wJCOb3*)  `_[[;wK S@}_:^4mk_Fqgg2:p(zTᣓT _8sIz3{C׭?g<`2X1D 9_rxЦ8F"XF=7MPCV1}}_uՊ8~#C OϕGRWv J\ڔPKhBbkjȾp?{(²eԥcPuf`[cNikr vVfBy.B#gppI$!NgxǢ+FӦmL­8*$5d ,gL)6Ij H'(9,DKr/y$v/D| 뉾q@UbCX)"o VQGvHvBTˏc HͰM?SAAQ-?w}$ :,/QZZ jUX`jLvְ37M9*& ‡$oW5q#.qV>[U URvf_1WmޓӇR<1j3ioyMQJ&Gl$}$~7_ٛz6X8-fYDb{$WL*A-k6on[jʚIU  b_q58LIIh]R8T`mcq3eӹN! @$K1\l䔣{(Ev/cc>?-d!1hEK%=cnO1!0̟OɎw .B-BoD^BzAᗆco{W+6j 'b=c<Ԫ9HB T:dwAA8Lw'Hl2/J86|5fOEo_<߳_߸@X $Վ?ٕ\5gcs.ylاH[rfKob5f-js xDrfcDz1B]ca^yLI=wlښvOEWb]DlD_GZUZ00qUI&)fpf}"k Foc5@)rFF4f  oiid2gE\ _iX{B.%UdfI* ||<AᐲWTTg8OC-6s/džC2$)'\mV A&gVasajIԉt$E3ТA,@b*1^b'۷Ȋߦgy @]_AZj8O/؆mPEZ4dzilP X>CAA# .]J,cܸq\}Æ FX*6МV5]|;f7(pSHF,Zc&  (7nk} @$6mt./3*I}u=2_` vA@$jomQr+Gy`X܏SrgV4U~"""ٸX^{xJ(X;{[ȟD,Bg\7߯{b  #yeye?}w"rjem }?~ uƬ2xjz j0Cl&=ldz3<+>B|-%t Ok3<\ESH6jmjo*J`u:O_S2ED&XrBk(ܷk!:;9nB3 XM*eq]DDDY)0j4Fhuyrlql6XIg*wxf?J` ,ķN \ʛ>p}MW1]fM8V.}e}?`-wukܵ/sSIvppUp;=܆ӭLDd 䄖͡2TξV w5ŋˊ?ul77q'W%"""K18_<z;C˿Ç[tEP&C*$xWcjfX}3*R S)XR=NQu$^m:&#W~Nj\~/& b/Y%WQ-? @`3|L.};Vty1J1sqP`"rP%'ND*A '<9p{h#IDATF u|uܦKDDDd3L7x~2\\%ygƙ瞶ţ_ǟCwa[̊ߋ'0#,M߇[gL_M#n}a6vo;@=KbSx\kI۔m6,"t023?WI<4U:wh?ɽ>]!1RpW վ;Āt՜abcBXOL01CVi` 0 ;6^q{0Lwn[*91)ތ<'Jp 毿t+ksǽky] DDDDd| LXuWq3 W!?pǜeY,F/e`iuO( ՌN}gv2w9`'pyBޔy=Y䋸k 7?d/xƜ[0?m_y3$6=C_d_ꝙ`+~|"OԮuYK;<-k* dy/'Ϥv?앤:w{67}x_I*oHW{s>t;o=I  I wcX381F g`<@3ܝy1ZrNBKUV@r5 Z =\; a'bᮬӅJkEz39tx ;߽oOÅ0LpZ#3Yc}\w}æa,NMEoqmr4,sk6fpha(WEc۰{"<&ۗu^-`Ù3k輩TX;Kkvr*HH0t^l 'J\չ maMXۈSySg~5/#gpy3:CxIDo(t?y,o`W{>/es* [y?[ѾW bĺDzyfHI0a?9ճ?}L?fܵ o1k_ap?Gpg=|Tʢsݾ6), 5@r̉k/m,kL{m їn^b*+>&w"O""2snFbN%1\ 7݋-!+@4MLh:ka< F0-Oswy]ng*krt^{hj6?ԁXfA%hm_Hl~;ڗu^S1 *I<,@3L7>/63?異qmP5qܽ~ۓN=]_Ƴ)ҲMM;xa>>z,\JFdNd"IDD&}.^Kˤ0sJܹc1{'f㫏|X7?E:qXL dRh(zCۖL}*sϸ5|KKsy}S;w=RȻEL \ʃۛF=nKlٹÞs2>u\^;Cʤ1`:v6FJrpFkϽD^6|3QDDDDDDDB(S5:k~09fŨc9{gGuT9nC}U,DX2)]9qYֶ?l#~cByEx}y៓LĎx}ۯݢ"""""""lj,.;+_힦xnKٻu=/.Ra, yfX"""""""ǁ,.v]žW9b*+ͼ/,8w>xa#ymjB,qK&.҃V'{yqkS^ș=ky/.}"""""""2DMpy݅Xc;tf0 3*OuLuNxp΢j׹6魘U+YDDDDDDDN c7\e<`0-alcs XW5egLŲl|~;.8t\GDDDDDDdS%'%0qUXs{^-~7ɢy19Eٽe,+ΜҙTm[MEDDDDDDz`I0 x5˫NwY] 1Mg\~^R3^[xs"""""""'XrR3 ͼ3+zZYŝźo:rc&n瑟 =x08NXlK)~lhuoGd0 kϚegL'Y}\ADDDDDDdR%a&N)_lKZI~gl2@(7N*G~qkL+0zxݺȤKd0l-a%ڟ]Aoxql=+uDDDDDDDN D㶅czTg``dv>YW0.2 àiW7/q},sOPWlU?u{Gu<4׳I!\`M}5ζxYc£M.@/O="=G49,QX-G|L DA3n6;~L{cP+o+{O,2pkLO^5E %6,SEDDDDDD&X"$ EPSlU?@s"V^;p/?vz\0M(ƈ`'gBuN+wVH׈+8O3 1 㨮v|g~ /D8ܢORpuFΪbR5}.m}+XVꨮ)c^""""""", s?IE;ohc嵿۶x k3gZ!}ZڡKDDDDDD&'X"I7/$2gہh'Y#c#W,,|oƪ?Xcfmwܷp|\ADDDDDDd"P%rysOR,qG:ΪqYKYzlY ^~qu̵ zEDDDDDD& X"Y//P>wVx߈`+9都B:Ě[Wm4y)y[) ;Zm|gՏ8~ir0LQ_'elnQ%"""""",wH//,bm@+Fo&y讯{˚ePW˷#{mh` |~ m{߫LbdSXFŴټؿsQ]q2/ܸL00/""""""S%.( ŷRwwWA!+?NA^yWXVꨮae=^>ꡈȉ@Ȼ4XJ7ҷ﮾H2uknk>i|޽Gz(""""""2)yK[:v{"f+*'c!}׹ʹW),wYy/\om{o _sуB,x4Lkf"#&w7]HYa{- DDDDDDġKdŷ 9۶kh2ul0sXgȐk4Ay"js'qIQ*bmT<ؼrιu?J]c߾o $Smݑ#y)@r*Orm᚟?(*,kn|L ʆaps/^gO{ܼqKdΩ䳋?Ip5uoGkN<:tJ=sN0h߳>>*8^Z"""""""X"PMn]| a!֦-xIbw=wmHO' yoWC!LL D&)|v~g&~$dֱ9,=F~/>| z/&7 v"""""""I65ތsձ;rDU`9 V\[<7:k-嚳HhB``Lpuܾ\^g_<(Ě}څ:oxTrTC mO^+x&*ں# """""""B dFt>xXQ +G}\_.=q]DDDDDDX)94cmoeM4].μ?}Q_07]g뽋 X"'YE3uGq ^o]/7ޓb=^V\IryLďEy~JG|utFDDDDDDD,F>9&܆7?dX^_4^h:X<}|7bȻB lnLn\Bo7ݛbTNmlz"G}|?_~bRŷ~ظ>ȑ(9+-?b5|VeO}? ;K HCD2uA"""""""D$0dALc-WAlvr9O}zbsxܮ#' D&=/-!VaκV:[[xb02"x` 5*C| '߮O!' D&*7fX<͟=b9b661]8߽1EA.8n߀?gTezm|0bEζYO<aͳQ_$3L(!'J&D&3N3Ȏ'xxY/+XC$b1_Dzmw:w&= K$t7ڬm82hKλ ?x|1_4 nb#ݫ +qK$qV2xMֶ?m{|0LByE$q^]6kԖw_L{wxܺ`DΙr&7̸*kۃ[ቖgF?mQ[߾o11v X"'skVp}Yo3O<|x9[)(~@_Wۘ1"oz XM,{,ygsMeYm~w|8Oy#  _?޳HL EDDDDD؜p֭[>ƢEX|977Mj/^g߰gϞwE& fmyv׾@+ƥ+;*w~h\! `pM7H$;=7Î۰a?8^z){|^dhڹ\Qwqֶ5=s{^v2sɹm"cF}U>_|"v߯tB3}cqw300w] HR|_[o|qK.Gz%KrJx>/2!]Zw>ov0 U;<++8Ox |= {Ԫ=\|x>Lr'Tֳ>˲e˜ K/Ų,^xC ***(**mlͩE&˦]%Mלg]I:[wC?JjƔS贚qg9yPֶmۘ>}zֶ^rBG__߈===9e˖,:ǻ2 !tj2>;튩y9 ]x?#2`iBD<#?w,c_#+GEA:`jiJ`ρ~ HY_IlJTP]eS z,4{!2gL$\彔**>}^W}}}5|+|sn{J&OKI,&3/`w5`jq{/X>k5MU2"ٖLZXRcxV.;d{GeIU!fO+$_j!~扈D%Ґ*: G4M/?/ί _җ߉93Ʋmkn6>y;2?q_ڙK=A]eu#{XPI yt+PZy~>CTí Aݯ*z/KnvnVZ[[7>˝n&?֭[ۙ6mW_}5Wv-**~aj,; {_2!_s3da")'stSz,Q|KPMn=o¶9z\z<J9["""""" `}k~B!n/fgYTz͚5cZo#/r"2 ͼ˶yikw%Jfc&],« _paC J'R ~['O˶( <W7RU"7}BDDDDDD &cJYtv۷&!&ͼLe[jye6eNLgmlxQϣfsM(h cp4Xg߳DbP<7衪8.EYaAΛ'99sId|LRQQl."0lZ iѺevQ#d}6м9{+*?i'dJYSrmaϝ =n?;wO6FS;9Oz bIes˙1˲1 lMzT51MgH,o뭫mͧ|E "DRܵ׬i_l|fL:>BŸ=^Fl=Qz̬-ymz'ŗ?&{IX"2!L\+x x*g@3~*V\} nݼ 4 m_,ۦ'ގtoV M*C\tZ V&$(` DDDDDdrP۠ i2>Dܹl|vL˫`<_>Fqq^cd6]}1p/n,eQC /ǝm6, RUbfM,lM.'"">Ddy/kbt"Hx/xs-E05[CCe\5g; [s$w_(HUI0@$†"<DK"c`M &L'D*M][mw-)l }5zI^\/"b&%,~7`w>ɔ嬊XY0pk<H$2>&{Ikbt"pOl |nVJF,eͼ(m.׻u'۶& xlގ ޅ̫+ͦvZZ2"(/ q+ؚ3ODD&}.^Rw.ZQҽ p2JuN%>۲l~Iֿ0nO ޛoZa1 n\7g߁jr{`Ww _|W/]]TPQV)"""""KXo*&L'X*Esvg['JmAUkdY)ZwѺ %/e53(IWhO$58$DcMoj00Ȝi|n맬0ף`x<H$2>&{IS'Xd|U4{kb[g['/)5esl۶)(}0Gx3\&w ގDbIn@iAx|n83ODD&}.^R5A(&3Ȣ(]}{w:|/LkEhLtgLĸOnAeYeSQ$S;WD̬뷜e߼IGoi_Ubδ" s}03ODD&}.^R5A(&3"(]}';z[m|n[q*csLӼ֖&Zw5tox9\nϑO&W7c_{;ׁ(_q Kxv^NUI C|j8~扈D%1YK &Xd| pǪ;ٷ+k{ 43_k6=n˔ m~ Jj),4jD'q&kxq>v p; _<xc`+Hr'"">Ddy/) `ML&p" `)3 YzBm{ikLkKI&\qWѶk `.EZK1,Ѽ;]?*sN )S]"4Ƀ-DK"cc6ɔe"O}_.5'{Ikb4 $ܹ4um;SrY3ϟ=HV*EWn+ގ}Cy6R^;B<'R ɔ͊$)ngIY鏷\9m]a>7A|C?DDd"璜,l&ĒDbIɔŌ)K &Xd|25xu:6vl{TLä.LV]~-nsl{Ҕ^m|KWLe qjf HZvZ{ p˕sMUlE^LA\iy'䪈'"">dlDq}_/h:ēDc)NSN~ˋvkX*/sʹ|4~fֹsgOwB_8&u*X]|.A*H,:z䅼V%Xi2>dskRf6w5g'I;uxMuNjLcl?c<iÿ`*kgR7t+Ie"1#]uz5W[x襝T΄[yL{o3ODD&}.bY6xh<6RԖظ~*K2z1*cn~̾t@UZ.syH¹c/azU2! 쩅,i,?`L@>7%2dy/) `ML&#FTm=;ܕZzwcsUAw43`٘\ҫ4)sӼݔΤr*.V›vejkg3UKgᮇrVC*Moaئ79 ڦ6 r%9Xʲ0 0h˄P骦?m dkWB"RR]όEgS5})'e[nwy=00ɔE~˷?{Jnlgȉv`I^ˬ,^̂bi-|N+>nא>&d*)ӿ G^0wvez;\1D~3&OOX)vp \rz-vvq[ x}n>._6 q9P mۓn^K%"2=eai:,][LGsq=>^k]k( 3>3封\\?N=ƥݶ֖ʹj"wox%Izr&A(ȭW,mm֎Nv#s>r !6". Q["21 {m ?%8aMsϯGyQ/޸±{׷?Dd^IuGIr^O/&KgCXʩ-q_fcF[2{{_\3W-]S+r3!_7xnמ5==%q9/5)|C:9(N/S*sJbD:]Gw H/}P3Y  iURTQ.t۶E7m-qJf.=w0nu^Aya/aO7e:SM%?#Wb47KMKdprݿ?SGB6 fN}Ͷt5ba 8֪9)ضM|Q#aG6}_oVO\>3V[̟^lT2(vce]t .TtrΘ[>jhd7]2Y6NKDNx%"Jqfiض;V6w5Ե-[$;yjIm!ix]~u|瓈Eh֖߳&wSV;Fʦ4↑}!v*S(mbl5AYAJퟹ~>=QpVFo؂cRY~8e*m;˦~<kьZXӫ\YG_i+]u,Q{ϛՇow{?<̦n) iP]+0,g峯fS;zqUp)5"'DҢ;b%I,ͭwt94+;[Yev<"$wycG9evqO*P,:ߩ NO;,/9>D- DdR1 r*8f)+ [{v#YŎv'qnOsNe P5}Uem+g 4~0:TNCضM*gnkic&^z<@Y w 4 J XؐS^dO{:w`m~7U C,Q<A=;z:hmiNWvu/tuVM#EGʆ uFr{R)n[O_\$;;//Ӌb|^םSLI\܁4Ƴ}pMr^$ /JyQ7aG'JfL9tx`aerz =w kprKsز ;18,qʬ2$O'y2mtņ U[@ui:ؼg9^ԗ`6/o؟>_==ΛYU8D@z}ah)`sK)Μ3}ݩ|nں#ǜ䇼S ; ]Rri Jþ9>|^hrY z\<,p"~0 70 "7`=aʋtfRyl3׶,=_G/Y D9z"JXҩj[WDNêv6te¦3J9?^M,>=|sS8efE{, eMlia%C2c}|iyLka`s\`Ici,+h2JsvgbxB4k_(>bP~1/s,<*Z[{sGH\GjڒkP^r1eBb^nZ:0eb zԡۆSAӮ~]79eVx*edFsj.xn>6 tCnzSesCG|en}nlwYu\z?WQ_?ywܻp,n:<u}sw־ N6׳n{8ӽ},/ᯯg;mvz1_w~\ }dY>g㗏egO-޿X<ŧ뙬p0?n;?|p=pN\xJ ? n%!%_"2cET %1 ^XɥOe^zhP(A^߾/;_C {xqpb.;c*Q~Ƭe|ƅ-uEŋN[;xa>'3HE^K4Wih .ng}OUp)B>8N0wtòm";[(/ y]nL-"ֳaG'_q+HW MSfa{'xy] *m]auy!/$ͻf514O$ yfԃcLuQVH\s55|?nIo8N@SLL[ٶ7ҿ Y̢t_̥qJIJRto!  `.e5 _2Y]D<h9AN`7/j<ZÅ˦8n>Bќd -}Q~ pz *Y =u`ki]`5ϓzfgfx'iE|3˝}0K3W>~j:Pv͢t+O%CA $ؙ{ zdʹZ6)Y!`^&\ y /]19ugO? W~=܏m\.\29b[6|~l:s(gLuö /$ _Pi{T{}nfO+vO .ե9$^w_u =|t ~} ~Okzp`6sOӴ8L1M f{,h뎌xm_a0egB!֮teky Ty]Nʧnmm{2 fL%S8ef?˶3GW'} kgR^ޚoic)_7Hoݽz~73]&6tg%3YUMMYZJʲ?w22J/M預\y46ө* Ԫ=Hɰ?EeR)΁#8d|DzNufc=GҁVa=3 yָ=JҾ{+m-imimu3жm`n3{Of.SPbY)(˅{q"DyG_i?n;Yt?#>uG?|+OuN}_/_|lΜ7жm.^ްإCS?eIc)]1vu0{ZQ=XM2i q0$ĒX6CN;6,XVheL@wX"mS''/7<&ե9XM皃]ng 2kVL#xgY62?{Q+D>Ddy/gHdRʃ=em3 ᛻OGշ]}{x\iyNCiy58Y@kK͙W| `.g]s+us:TA2}V|k8_ 2^*p̋ށ8Iˢ'ʚhj4 nr3%upe72"0h#/O)Zi }je1±3 2}k?߻ۮϢVIH`#0ɭ& j!eѻYp{NvjĸbkvlO5=9ZSrj֖&LEeus:)V*9thZ:G~uV=Òsͧٶ%.+9>=km02N圅UM-. q)<.^؊뢻/+[NZwܷEEqlOIϘ:1nɭW?o{SWeT`颾`Ӹ8[{v3Co/ŭ;ؙntYpfa+ZEۮ-WN)3}L10;2K˦IĢxA+QX^CQy-n縜{&t%j͞"""GeS9gQ7 ^IN/j䌹崴g WSj?{z8qQQ`n].sE>}.WVQW_y)9N./]@8fK66ghu\8aMzִ ߛGcaϸ\oz$bjIi o ۶0 X'~$Q*jY샸lۢ~3>>2ۙ`u_:ξj*"r^cP_^)rƚK {z]-""2q(y=Aca<zb}4u5;S;]Z\k+9csӿ v8R/^N DDDDDKD]ԊŜZtC̔þD}J/mQ[\uұ'y Wnv\ ^pomsm\i9/"""""NP%"2A) tlf@ᖮmiۣۏx)LuBMelzK0 6[T5>_@f잭زfvSg9ضJ,y( à* έYAJ_ŴZJC=LM][YP:}?ctK5 lta[6嵍#t5ɫ;Rg"}sEXao DDN.ŴZH%h`@+_Ŧ-|qɧYcμvڂ'~48>'ͼ/e߄ip3_и[د)i6Uhȸ1lTʢsݾ6), 5@2i۷3.&3M6z_4e{N./Sr񹎼U*mܞc{-+=Oog+;7FkK[mEyDZ,X@(NK'H$2>&{(5>35T%"2 ~fH8n# Q(ez\>q#WyE_~C<}wSպ'~W:YS̻p DD&'iDwUz;YײଫmKx&{IX""rBr.k˯iH%ֳәro7=:a%)~Ypfa _HģJ&lY,kFjf """""DXo*&L^#9H2Js6!}G5.דCca}fa%1_}Vvm-M0 ~1μT2m۸=1xIDD&}.^RLJ%s_2xf5u5u\_7F4f ڥV0Ikfr ؽe הTQV;F*j1a*U`ML&Fr:"]4uoesg3M][ոPyfa=3 zc@o'{Ѻk3IģLiX>A*wO""2sId|LxV`)z`ML&F2lۦ5Tg5um%qAMn?`^O ](y R^3>X$"">Ddy/) `ML&Fr?acQ=Sᡬ}!v5YLY,vnz[V! ʦP1u&dv|>5牌^BwtQ?\2|zv{hu5wר+&$k lόzgaeн[ p_c1;md""""rbPAM'L^#!mncSW3M][iJ$98)*n`jI%]xMAw;@ BMtmBy$1voYee߄a"i|9Ѽy<3RZ]eJ%i^S adTDd牌^RwI0 *BTY9e9mo{; +1bξ];}~K$C-w`av z2eB0V1.4IGyoxd_f|IUμX$`bEX#$]wcjݕ!ϤV: !|LDDDDX"""iLͫaj^ N]IJřr>RqbG>v{ 2U]n(!?y¯;U _ïj JG=~T*LiGø~4,$<}xT2"Oܝu{gI%e"^CaLEPv\xxNcFNȕ6 03_a2h\%+GW9}.Vr"Cρ}1 yO]]v KV761D(t/y`1q.r9zs<6]?( gFESЕh ddc1=~`VFNvPSPZ5>tOnj _AÂC_ kH%D{i+amϾJ^?Jna7r*A ˦_\I* L|HX"""KW}R0)+E$]uHK٩''7骸D`mɜr JF=tJ=s׶mgst:؊ 8]^NvmLJRUҶk C_%fֽJ7TTH%./IJp\3D'4汶m`+1zѦ<,h ;`?B1wQ<0 ;Xsae53ye2o٥@H%YIl g^qW<:cH .UOǶu/=ddžW2_̡ƹꥈ= DDDb^KoRcfUx `ÿdU%cWJ|æ4xpeJt92 #+ i\t/Wx4/~ڜ((" y YW(+n ?J& ξ Z[ܟ>[U(EDDR%"""2a ytWrشǃBİ >`C$tWH2J']c6\x!XiH_ɱtJ=W/YW21e9W<0]Mƶh\+cNGN r`Ӂ+D 'K+}zC_.WXE4Mx !AFV9FI;E_D1Y9l^sT__/;E\³&9S=t?/0%+GGs$ߝuso,e53xտ2ҕ]f3uR0wnr;2{l+L DDDDNa z4>Jd¯p:4=˞IF3h*_+=._Q{~*2WP 9CyE9C:W}j J֟ u[@_W/gY\w?" ; WTNog+n"KDDNx DDDDN||_ZE,LyEf Rvcsy Fp𹼇 |TMK+u&LeE5 vN<q17¦מ:_92ˏ @9C- DDDi DDDDL̬^8=DjWL>xVRqb8]5 sXBDV|n;C3.a̔t)<19%૫0m;ˏg3],fe7صLRXVC٤ ѽ/9^`q1xy^e6Tl_#CSES#_Y5c.A^ç7 0k@LJ;7j+K>t{9ۈG+/ҡeYv:S9m6Wrc\9\ɯb&k#p|y!*"'XdT2V%""KDDDD&40MދcC+;W~<Xvf֑/r8TXϘǦ^cfS\B% qvXx #d<cSY~'/fۺYr2cKϻx4gLw :=`.b\n.5,ܦ\oޜ1mX*>G!vac5=#FCXAˇ \nEΩ/k۶33Bkg-l PSP @2c=/~v`Nଫ>۶aߗ?DNA1e5ضM*MDD DDDDD0 n~B <>e"vxxC^ѓ̓:\W0 ?7׶mEʮg]M+cJOQdwaMuV\}3pryڟ}=>9=1r \lVȻDq2]xCxCck6q+p N N<8 +xuBm 0)R99նG#ؙ0q&J`.}ߢx,|v{:ֽȪsB/?Hiu=_Jͧ;z!1Gi/""cKDDDDd2 ˋe읿2_h& :lx@SLVx=150C^ DR <fz/XE"!HO?-cYWe57tEX"a㫏Ewmct bLz;[9g[VE??8EDD&+X"""""tc񄀑m$İ`1eag1wr<^6?hG& Q p c<>?53_R"ģX$nu) ϑJ%y6WkYWi۵x,2+/R՗L. DDDDD$ax]^./1lC!liI+y*-i%{ _@ʏa:zT4̣q=[ƴ9e*C۲6紬y6wl:ߢ2sɹݶu/<^^Jҫ<w+hwMe8fJf8Žu3p?E as?)9UY}lBJ0dm[$a2]]>ʃFDS1\ic C䄤N"""""2L$ i|"k+{p2J$& ̢(vwIF$tKe(!Xfc_. u$8+?w5̬vVl+*Ua%UutǁpdɈ]K(?DǾYS+RZ=h`d "[a``+a%y`H Y{_a@|Rvx?);EMN^q}y< :]Y֯6p{ֵn?Kq}Õx]g{svX=5p.nqgOY"만Lx|E~,_+\#` +q(-iKӗ?~ Fp _q1wɪ1]nם+]J9y9h`n!i۵{+H$SaNs i^&aR5}. jcMii\ۺET̜tQ9m6P{nwI0r8=2s9-, Ϭ0i:Lo6Bg`SzskIF[Cgk؊Ppt[OOCot;8LSrԂStzyzk*2_ǏcWNџ?1#0˛~+C^'d`Nߥv?f9)(ӗt@r˧pM>MW϶mcۖs3/gibe3۶P->tfY wX$=6UP>:l"biYEeyu`m ?Γ-ύZEu60YahXmsnzG\uh>G~]ɩ勹cOh S,ŽR+`vQ#O|fDu˶pgo~O:nDeF?p3 ^Q4e) l"7O-[Ȃr*,;v&@8/@NVhY=pV*33}oEg.'X-`^!ŕ›O!*ͦfD/O}7d[tε\z.{׎"[X6>ec3|ʩɹ7~2>{˪b{: Wն1+r9HģNˮ '3X""""""ttl&e5?x/& Oŏ=JJ92 U9-Eca=<^k]%SsV2L<4Hoz0z,9ǥl 15=M`bҟ` m"ؔ~QVSig'qyҡۚ*BB0ф4464L"X̮sCW&IXI9iշlf,:۶ʻA>@Ͷ,<ι9_ R鰮 3Y? e89UXv*=+;ұYMfT=ws&suZ 7`u;s7&;0}pMNStܮ,&v;Űm[\Q*e9n2Mq"Qn2>dHd$"">=} ]IH%YqgZ?U}9 qy&RMsrMn,`ȄKDDDDDDDD&4X""""""""2) MLh DDDDDDDDdBS%"""""""",`ȄKDDDDDDDD&4X""""""""2) MLh DDDDDDDDdBS%"""""""",`ȄKDDDDDDDD&4X""""""""2) MLh DDDDDDDDdBS%""""""""a۶nĉʶm,\&nƸ45?z?D%1Ki`ƸKLhB("""""""",`ȄKDDDDDDDD&4X""""""""2){8i)"c'ie,㢖XhKEA$uMYY̘.ә4HSrD3˕F /^FDCE.({û `hX04, FC#`ݥ>^{MVDDKE>3%%%W^WttV^-lЀsΩW^2L׿er&g:t{1S/m].رDEERSSճgO_~Ztm0Ǝa?S 0@_|am[(,,֭[&dddC>}\]]}v͘1CǎSrrUt钭]v)99Y RJJN>{O#GTNN>[5f,QYYx988ZnCٳ60ƌa7mڤ3f(11QUrro;sSO.pSjkkeoyjƍ6敗ͪmƌݻ- c(**s=iӦ_իk벀&^ӗ_~ﲳ$ܹS#FPVVl\!N֘qFmذA-[E5f ;`hܹcԪU+mQ]N t~/I]vٳgUYYip-SΝm] $^IRV$Ϩ_cWֳ>Kx4zcb:tHO?1Ԏ;T]]}J5;+PPPv֥ yyyڿƏoR&kȐ!***RVVΜ9b͛7O uy\IIN8!WWW%&&G=zЫsٺ<8p@K/^+>%)??_9rKo+%%`?IsUPP맓'O*==].p+++$͞=[...JOOWJJ4c Wǰ?$긺w,q;h.WXhZng}֥MW_}S*&&F˖-{ァZ3Fϟuy\mm3GfϞ0մiӴiӦrp-aRQQZpWSVZZKΜ9#I*++u99::ڲDHMMUhhOniדO>+66ֆv...PIb~1lљ3gԶm[7!;vΜ9UVY41ŋ3fL}ÇmPo߾Vm>\]]?ڨ*.swwW…X `\>ÒX\ݬY2&F&Mҁvٺ$L׮][}5kfΜ)___U4=۷w}gVZZSNC6 ˚7opرê}풤nݺ٢,P7uwwԯ_?K{nn®ߩRUUUںu˃g*//OԣG~fΜ/BӧOٳg_[y{{ߕ?F\oynݺRSSէO>}ڲ? ֘Drr4yd C#``hX04, Fpv%ɤ<[(eee8qBBBd2aYpL&m] 0(,`8k֬d?^o||"""lPY3k,m۶Mcƌ;C'x%0,`XJKKuMΝ;շo_5JuI7 V׮]}YXw[ɓ'|Kn=^OMMo˵0cǪV_z%yxx(%%Ef*Uk׮URR,X+88XNpEGG_yjӦMӧOP*((У>*IϗUB=rttѣu=hժUWff3sLi 5bhҥrssSUU^x?~\qqqz衇g͛7O'N+bǚ5ktĨyrqqQvvRSS5` >\.\о}7(220$_^zWoYݻwכo)IU>}o_֘1c$Iz'^O?)77WNNN$oooM4I>|fx ?$)..N<ϟ_ovף>s^4)++KAAAC***JfR߾}.wwwM:U |rK_}5k={*??_?V``ŋO.I4hz)͙3GVqqqQFFZGQQ^|EkNK,$飏>Rqq֮]+OOO˳|d9R=cǎo,m[lQ.]`>c`,B ]QQQ[s=g|||d6ڝչsg;РAJz)mV[n$}:t"##u)\ ݻ^}.2uVu^IbccUZZ~q u}޽[WϗtB˱.]җ_~~Y+Iz={:111 WWa $)//Orvv oܸqڰan,[mjJ{oУUV:}t=<ODohfе֣jH9SNU׮]zL˖-έ;wqGUTT-[q{0`֮]zjkkѣG_ܺ ԭCv%///i˖-ڶm׿jŊ?~&Nx7n,$$%%iÆ W"a, #G9|նlÇ-_{I?-vuzAAAZf6mڤK.) @ XM-Zh{{{ugԩrpp̙3hzNTYYe˖-5p@ 8P՚0a/^c<@ c ,$tIQQQZjN8aIXVϺuwӉ'ԫW/I:uꤥKܹs+o7wڻwciTvv:t_W7oV2Ljժ_(ܱc[͛7^VV7*00jx뭷4`M>]7o?ڳgmV \SNYm7o\^^^2ͺx n/f` QׯU./--M|||ՙA !CɓZl<<<#+JHHPDD vڵkx⛺1ci&%$$(>>^...ZnJJJp«j۶sm޼e?7j(iFjݺ8L6p{`&CQQQZvm}ǏWyy>s}gի>o>ܹs 믿-ZX ѪU+33Sj۶w؛v6mrJ͙3GpL&/^'|W```i֭ZhzwEYYY;w>fu]s̹BѬY3-X@ 7n222˗>P^^֭[''''yzzj„ b}Gԃ>x7fV%n``hX04, FC#``hX04, FC#``hX04,_" 궠IENDB`mpire-2.10.2/mpire/000077500000000000000000000000001461637447300140435ustar00rootroot00000000000000mpire-2.10.2/mpire/__init__.py000066400000000000000000000001111461637447300161450ustar00rootroot00000000000000from multiprocessing import cpu_count from mpire.pool import WorkerPool mpire-2.10.2/mpire/async_result.py000066400000000000000000000226611461637447300171370ustar00rootroot00000000000000import collections import itertools import queue import threading from typing import Any, Callable, Dict, List, Optional, Union from mpire.comms import EXIT_FUNC, INIT_FUNC job_counter = itertools.count() class AsyncResult: """ Adapted from ``multiprocessing.pool.ApplyResult``. """ def __init__(self, cache: Dict, callback: Optional[Callable], error_callback: Optional[Callable], job_id: Optional[int] = None, delete_from_cache: bool = True, timeout: Optional[float] = None) -> None: """ :param cache: Cache for storing intermediate results :param callback: Callback function to call when the task is finished. The callback function receives the output of the function as its argument :param error_callback: Callback function to call when the task has failed. The callback function receives the exception as its argument :param job_id: Job ID of the task. If None, a new job ID is generated :param delete_from_cache: If True, the result is deleted from the cache when the task is finished :param timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default) """ self._cache = cache self._callback = callback self._error_callback = error_callback self._delete_from_cache = delete_from_cache self._timeout = timeout self.job_id = next(job_counter) if job_id is None else job_id self._ready_event = threading.Event() self._success = None self._value = None if self.job_id in self._cache: raise ValueError(f"Job ID {job_id} already exists in cache") self._cache[self.job_id] = self def ready(self) -> bool: """ :return: Returns True if the task is finished """ return self._ready_event.is_set() def successful(self) -> bool: """ :return: Returns True if the task has finished successfully :raises: ValueError if the task is not finished yet """ if not self.ready(): raise ValueError(f"{self.job_id} is not ready") return self._success def wait(self, timeout: Optional[float] = None) -> None: """ Wait until the task is finished :param timeout: Timeout in seconds. If None, wait indefinitely """ self._ready_event.wait(timeout) def get(self, timeout: Optional[float] = None) -> Any: """ Wait until the task is finished and return the output of the function :param timeout: Timeout in seconds. If None, wait indefinitely :return: Output of the function :raises: TimeoutError if the task is not finished within the timeout. When the task has failed, the exception raised by the function is re-raised """ self.wait(timeout) if not self.ready(): raise TimeoutError if self._success: return self._value else: raise self._value def _set(self, success: bool, result: Any) -> None: """ Set the result of the task and call any callbacks, when provided. This also removes the task from the cache, as it's no longer needed there. The user should store a reference to the result object :param success: True if the task has finished successfully :param result: Output of the function or the exception raised by the function """ self._success = success self._value = result if self._callback and self._success: self._callback(self._value) if self._error_callback and not self._success: self._error_callback(self._value) self._ready_event.set() if self._delete_from_cache: del self._cache[self.job_id] class UnorderedAsyncResultIterator: """ Stores results of a task and provides an iterator to obtain the results in an unordered fashion """ def __init__(self, cache: Dict, n_tasks: Optional[int], job_id: Optional[int] = None, timeout: Optional[float] = None) -> None: """ :param cache: Cache for storing intermediate results :param n_tasks: Number of tasks that will be executed. If None, we don't know the lenght yet :param job_id: Job ID of the task. If None, a new job ID is generated :param timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default) """ self._cache = cache self._n_tasks = None self._timeout = timeout self.job_id = next(job_counter) if job_id is None else job_id self._items = collections.deque() self._condition = threading.Condition(lock=threading.Lock()) self._n_received = 0 self._n_returned = 0 self._exception = None self._got_exception = threading.Event() if self.job_id in self._cache: raise ValueError(f"Job ID {job_id} already exists in cache") self._cache[self.job_id] = self if n_tasks is not None: self.set_length(n_tasks) def __iter__(self) -> "UnorderedAsyncResultIterator": return self def next(self, block: bool = True, timeout: Optional[float] = None) -> Any: """ Obtain the next unordered result for the task :param block: If True, wait until the next result is available. If False, raise queue.Empty if no result is available :param timeout: Timeout in seconds. If None, wait indefinitely :return: The next result """ if self._items: self._n_returned += 1 return self._items.popleft() if self._n_tasks is not None and self._n_returned == self._n_tasks: raise StopIteration if not block: raise queue.Empty # We still expect results. Wait until the next result is available with self._condition: while not self._items: timed_out = not self._condition.wait(timeout=timeout) if timed_out: raise queue.Empty if self._n_tasks is not None and self._n_returned == self._n_tasks: raise StopIteration self._n_returned += 1 return self._items.popleft() __next__ = next def wait(self) -> None: """ Wait until all results are available """ with self._condition: while self._n_tasks is None or self._n_received < self._n_tasks: self._condition.wait() def _set(self, success: bool, result: Any) -> None: """ Set the result of the task :param success: True if the task has finished successfully :param result: Output of the function or the exception raised by the function """ if success: # Add the result to the queue and notify the iterator self._n_received += 1 self._items.append(result) with self._condition: self._condition.notify() else: self._exception = result self._got_exception.set() def set_length(self, length: int) -> None: """ Set the length of the iterator :param length: Length of the iterator """ if self._n_tasks is not None: if self._n_tasks != length: raise ValueError(f"Length of iterator has already been set to {self._n_tasks}, " f"but is now set to {length}") # Length has already been set. No need to do anything return with self._condition: self._n_tasks = length self._condition.notify() def get_exception(self) -> Exception: """ :return: The exception raised by the function """ self._got_exception.wait() return self._exception def remove_from_cache(self) -> None: """ Remove the iterator from the cache """ del self._cache[self.job_id] class AsyncResultWithExceptionGetter(AsyncResult): def __init__(self, cache: Dict, job_id: int) -> None: super().__init__(cache, callback=None, error_callback=None, job_id=job_id, delete_from_cache=False, timeout=None) def get_exception(self) -> Exception: """ :return: The exception raised by the function """ self.wait() return self._value def reset(self) -> None: """ Reset the result object """ self._success = None self._value = None self._ready_event.clear() class UnorderedAsyncExitResultIterator(UnorderedAsyncResultIterator): def __init__(self, cache: Dict) -> None: super().__init__(cache, n_tasks=None, job_id=EXIT_FUNC, timeout=None) def get_results(self) -> List[Any]: """ :return: List of exit results """ return list(self._items) def reset(self) -> None: """ Reset the result object """ self._n_tasks = None self._items.clear() self._n_received = 0 self._n_returned = 0 self._exception = None self._got_exception.clear() AsyncResultType = Union[AsyncResult, AsyncResultWithExceptionGetter, UnorderedAsyncResultIterator, UnorderedAsyncExitResultIterator] mpire-2.10.2/mpire/comms.py000066400000000000000000001025211461637447300155340ustar00rootroot00000000000000import collections import ctypes import multiprocessing as mp import queue import threading import time from typing import Any, Callable, Dict, List, Optional, Tuple, Union from mpire.context import RUNNING_WINDOWS from mpire.params import WorkerMapParams from mpire.signal import DelayedKeyboardInterrupt # Pill for killing workers POISON_PILL = '\0' # Pill for letting workers know the work is done so they can send cached stats NON_LETHAL_POISON_PILL = '\1' # Pill for letting workers know that the next item in the queue are new map params NEW_MAP_PARAMS_PILL = '\2' # Pill for letting workers know that the next item in the queue is a task being supplied by an apply function, which # need to be processed slightly differently APPLY_PILL = '\3' # Fixed job IDs for the main process, worker_init, and worker_exit functions MAIN_PROCESS = -1 INIT_FUNC = -2 EXIT_FUNC = -3 class WorkerComms: """ Class that contains all the inter-process communication objects (locks, events, queues, etc.) and functionality to interact with them, except for the worker insights comms. Contains: - Progress bar comms - Tasks & (exit) results comms - Exception handling comms - Terminating and restarting comms General overview of how the comms work: - When ``map`` or ``imap`` is used, the workers need to return the ``idx`` of the task they just completed. This is needed to return the results in order. This is communicated by using the ``_keep_order`` boolean value. - The main process assigns tasks to the workers by using their respective task queue (``_task_queues``). When no tasks have been completed yet, the main process assigns tasks in order. To determine which worker to assign the next task to, the main process uses the ``_task_idx`` counter. When tasks have been completed, the main process assigns the next task to the worker that completed the last task. This is communicated by using the ``_last_completed_task_worker_id`` deque. - Each worker keeps track of whether it is running a task by using the ``_worker_running_task`` boolean value. This is used by the main process in case a worker needs to be interrupted (due to an exception somewhere else). When a worker is not busy with any task at the moment, the worker will exit itself because of the ``_exception_thrown`` event that is set in such cases. However, when it is running a task, we want to interrupt it. The RLock object is used to ensure that there are no race conditions when accessing the ``_worker_running_task`` boolean value. E.g., when getting and setting the value without another process doing something in between. - Each worker also keeps track of which job it is working on by using the ``_worker_working_on_job`` array. This is needed to assess whether a certain task times out, and we need to know which job to set to failed. - The workers communicate their results to the main process by using the results queue (``_results_queue``). Each worker keeps track of how many results it has added to the queue (``_results_added``), and the main process keeps track of how many results it has received from each worker (``_results_received``). This is used by the workers to know when they can safely exit. - Workers can request a restart when a maximum lifespan is configured and reached. This is done by setting the ``_worker_restart_array`` boolean array. The main process listens to this array and restarts the worker when needed. The ``_worker_restart_condition`` is used to signal the main process that a worker needs to be restarted. - The ``_workers_dead`` array is used to keep track of which workers are alive and which are not. Sometimes, a worker can be terminated by the OS (e.g., OOM), which we want to pick up on. The main process checks regularly whether a worker is still alive according to the OS and according to the worker itself. If the OS says it's dead, but the value in ``_workers_dead`` is still False, we know something went wrong. - The ``_workers_time_task_started`` array is used to keep track of when a worker started a task. This is used by the main process to check whether a worker times out. - Exceptions are communicated by using the ``_exception_thrown`` event. Both the main process as the workers can set this event. The main process will set this when, for example, a timeout has been reached when running a ``map`` task. The source of the exception is stored in the ``_exception_job_id`` value, which is used by the main process to obtain the exception and raise accordingly. - The workers communicate every 0.1 seconds how many tasks they have completed. This is used by the main process to update the progress bar. The workers communicate this by using the ``_tasks_completed_array`` array. The ``_progress_bar_last_updated`` datetime object is used to keep track of when the last update was sent. The ``_progress_bar_shutdown`` boolean value is used to signal the progress bar handler thread to shut down. The ``_progress_bar_complete`` event is used to signal the main process and workers that the progress bar is complete and that it's safe to exit. """ # Amount of time in between each progress bar update progress_bar_update_interval = 0.1 def __init__(self, ctx: mp.context.BaseContext, n_jobs: int, order_tasks: bool) -> None: """ :param ctx: Multiprocessing context :param n_jobs: Number of workers :param order_tasks: Whether to provide tasks to the workers in order, such that worker 0 will get chunk 0, worker 1 will get chunk 1, etc. """ self.ctx = ctx self.n_jobs = n_jobs self.order_tasks = order_tasks self._initialized = False # Whether or not to inform the child processes to keep order in mind (for the map functions) self._keep_order = self.ctx.Value(ctypes.c_bool, False, lock=True) # Queue to pass on tasks to child processes. We keep track of which worker completed the last task and which # worker is working on what task self._task_queues: List[mp.JoinableQueue] = [] self._task_idx: Optional[int] = None self._worker_running_task: List[mp.Value] = [] self._last_completed_task_worker_id = collections.deque() self._worker_working_on_job: Optional[mp.Array] = None # Queue where the child processes can pass on results, and counters to keep track of how many results have been # added and received per worker. results_added is a simple list of integers which is only accessed by the worker # itself self._results_queue: Optional[mp.JoinableQueue] = None self._results_added: List[int] = [] self._results_received: Optional[mp.Array] = None # Array where the child processes can request a restart self._worker_restart_array: Optional[mp.Array] = None self._worker_restart_condition = self.ctx.Condition(self.ctx.Lock()) # List of Event objects to indicate whether workers are alive self._workers_dead: Optional[mp.Array] = None # Array where the child processes indicate when they started a task, worker_init, and worker_exit used for # checking timeouts. The array size is n_jobs * 3, where [worker_id * 3 + i] is used for indexing. i=0 is used # for the worker_init, i=1 for the main task, and i=2 for the worker_exit function. self._workers_time_task_started: Optional[mp.Array] = None # Lock object such that child processes can only throw one at a time. The Event object ensures only one # exception can be thrown self.exception_lock = self.ctx.Lock() self._exception_thrown = self.ctx.Event() self._exception_job_id: Optional[mp.Value] = None self._kill_signal_received = self.ctx.Value(ctypes.c_bool, False, lock=True) # Array where the number of completed tasks is stored for the progress bar self._tasks_completed_array: Optional[mp.Array] = None self._progress_bar_last_updated: Optional[float] = None self._progress_bar_shutdown: Optional[mp.Value] = None self._progress_bar_complete: Optional[mp.Event] = None ################ # Initialization ################ def is_initialized(self) -> bool: """ :return: Whether the comms have been initialized """ return self._initialized def reset(self) -> None: """ Resets initialization state. Note: doesn't actually reset the comms, just resets the state. """ self._initialized = False def init_comms(self) -> None: """ Initialize/Reset comms containers. Threading doesn't have a JoinableQueue, so the threading context returns a multiprocessing.JoinableQueue instead. However, in the (unlikely) scenario that threading does get one, we explicitly switch to a multiprocessing.JoinableQueue for both the exception queue and progress bar tasks completed queue, because the progress bar handler needs process-aware objects. """ # Task related self._task_queues = [self.ctx.JoinableQueue() for _ in range(self.n_jobs)] self._worker_running_task = [ self.ctx.Value(ctypes.c_bool, False, lock=self.ctx.RLock()) for _ in range(self.n_jobs) ] self._worker_working_on_job = self.ctx.Array('i', self.n_jobs, lock=True) # Results related self._results_queue = self.ctx.JoinableQueue() self._results_added = [0 for _ in range(self.n_jobs)] self._results_received = self.ctx.Array('L', self.n_jobs, lock=self.ctx.RLock()) # Worker status self._worker_restart_array = self.ctx.Array(ctypes.c_bool, self.n_jobs, lock=True) self._workers_dead = self.ctx.Array(ctypes.c_bool, self.n_jobs, lock=True) self._workers_dead[:] = [True] * self.n_jobs self._workers_time_task_started = self.ctx.Array('d', self.n_jobs * 3, lock=True) # Exception related self._exception_thrown.clear() self._exception_job_id = self.ctx.Value('i', 0, lock=True) self._kill_signal_received.value = False # Progress bar related self._tasks_completed_array = self.ctx.Array('L', self.n_jobs, lock=self.ctx.RLock()) self._progress_bar_last_updated = time.time() self._progress_bar_shutdown = self.ctx.Value(ctypes.c_bool, False, lock=True) self._progress_bar_complete = self.ctx.Event() self.reset_progress() self._initialized = True def reset_progress(self) -> None: """ Resets the task_idx and last_completed_task_worker_id """ self._task_idx = 0 self._last_completed_task_worker_id.clear() self._tasks_completed_array[:] = [0] * self.n_jobs self.clear_progress_bar_shutdown() self.clear_progress_bar_complete() ################ # Progress bar ################ def task_completed_progress_bar(self, worker_id: int, progress_bar_last_updated: float, progress_bar_n_tasks_completed: Optional[int] = None, force_update: bool = False) -> Tuple[float, int]: """ Signal that we've completed a task every 0.1 seconds, for the progress bar :param worker_id: Worker ID :param progress_bar_last_updated: Last time the progress bar update was send :param progress_bar_n_tasks_completed: Number of tasks completed since last update :param force_update: Whether to force an update :return: Tuple containing new last updated time and number of tasks completed since last update """ # If it's not forced we updated the number of completed tasks if not force_update: progress_bar_n_tasks_completed += 1 # Check if we need to update now = time.time() if force_update or (now - progress_bar_last_updated) > self.progress_bar_update_interval: with self._tasks_completed_array.get_lock(): self._tasks_completed_array[worker_id] += progress_bar_n_tasks_completed progress_bar_last_updated = now progress_bar_n_tasks_completed = 0 return progress_bar_last_updated, progress_bar_n_tasks_completed def get_tasks_completed_progress_bar(self) -> Union[int, str]: """ Obtain the number of tasks completed by the workers. As the progress bar handler lives inside a thread we don't poll continuously, but every 0.1 seconds. :return: The number of tasks done or a poison pill """ # Check if we need to wait a bit for the next update time_diff = time.time() - self._progress_bar_last_updated if time_diff < self.progress_bar_update_interval: time.sleep(self.progress_bar_update_interval - time_diff) # Sum the tasks completed and return while (not self.exception_thrown() and not self.kill_signal_received() and not self._progress_bar_shutdown.value): n_tasks_completed = sum(self._tasks_completed_array) self._progress_bar_last_updated = time.time() return n_tasks_completed return POISON_PILL def signal_progress_bar_shutdown(self) -> None: """ Signals the progress bar handling process to shut down """ self._progress_bar_shutdown.value = True def clear_progress_bar_shutdown(self) -> None: """ Clears the progress bar shutdown signal """ if self._progress_bar_shutdown is not None: self._progress_bar_shutdown.value = False def signal_progress_bar_complete(self) -> None: """ Signal that the progress bar is complete """ self._progress_bar_complete.set() def clear_progress_bar_complete(self) -> None: """ Clear that the progress bar is complete """ if self._progress_bar_complete is not None: self._progress_bar_complete.clear() def wait_until_progress_bar_is_complete(self) -> None: """ Waits until the progress bar is completed """ if self._progress_bar_complete is not None: self._progress_bar_complete.wait() ################ # Order modifiers ################ def signal_keep_order(self) -> None: """ Set that we need to keep order in mind """ self._keep_order.value = True def clear_keep_order(self) -> None: """ Forget that we need to keep order in mind """ self._keep_order.value = False def keep_order(self) -> bool: """ :return: Whether we need to keep order in mind """ return self._keep_order.value ################ # Tasks & results ################ def add_task(self, job_id: Optional[int], task: Any, worker_id: Optional[int] = None) -> None: """ Add a task to the queue so a worker can process it. :param job_id: Job ID or None :param task: A tuple of arguments to pass to a worker, which acts upon it :param worker_id: If provided, give the task to the worker ID """ worker_id = self._get_task_worker_id(worker_id) with DelayedKeyboardInterrupt(): task = (job_id, task) if job_id is not None else task self._task_queues[worker_id].put(task, block=True) def add_apply_task(self, job_id: int, func: Callable, args: Tuple = (), kwargs: Dict = None): """ Add a task to the queue so a worker can process it. First though, add an APPLY_PILL such that the worker knows it needs to treat this task differently. :param func: Function to apply :param job_id: Job ID :param args: Arguments to pass to the function :param kwargs: Keyword arguments to pass to the function """ if kwargs is None: kwargs = {} worker_id = self._get_task_worker_id() self.add_task(None, APPLY_PILL, worker_id) self.add_task(job_id, (func, (args, kwargs)), worker_id) def _get_task_worker_id(self, worker_id: Optional[int] = None) -> int: """ Get the worker ID for the next task. When a worker ID is not present, we first check if we need to pass on the tasks in order. If not, we check whether we got results already. If so, we give the next task to the worker who completed that task. Otherwise, we decide based on order :return: Worker ID """ if worker_id is None: if self.order_tasks or not self._last_completed_task_worker_id: worker_id = self._task_idx % self.n_jobs self._task_idx += 1 else: worker_id = self._last_completed_task_worker_id.popleft() return worker_id def get_task(self, worker_id: int) -> Any: """ Obtain new chunk of tasks. Occasionally we check if an exception has been thrown. If so, we should quit. :param worker_id: Worker ID :return: Chunk of tasks or None when an exception was thrown """ while not self.exception_thrown(): try: return self._task_queues[worker_id].get(block=True, timeout=0.01) except queue.Empty: pass return None def task_done(self, worker_id: int) -> None: """ Signal that we've completed a task :param worker_id: Worker ID """ self._task_queues[worker_id].task_done() def set_worker_running_task(self, worker_id: int, running: bool) -> None: """ Set the task the worker is currently running :param worker_id: Worker ID :param running: Whether the worker is running a task """ self._worker_running_task[worker_id].value = running def get_worker_running_task_lock(self, worker_id: int) -> mp.RLock: """ Obtain the lock for the worker running task :param worker_id: Worker ID :return: RLock """ return self._worker_running_task[worker_id].get_lock() def get_worker_running_task(self, worker_id: int) -> bool: """ Obtain whether the worker is running a task :param worker_id: Worker ID :return: Whether the worker is running a task """ return self._worker_running_task[worker_id].value def signal_worker_working_on_job(self, worker_id: int, job_id: int) -> None: """ Signal that the worker is working on the job ID :param worker_id: Worker ID :param job_id: Job ID """ self._worker_working_on_job[worker_id] = job_id def get_worker_working_on_job(self, worker_id: int) -> int: """ Obtain the job ID the worker is working on :param worker_id: Worker ID :return: Job ID """ return self._worker_working_on_job[worker_id] def add_results(self, worker_id: Optional[int], results: List[Tuple[Optional[int], bool, Any]]) -> None: """ Add results to the results queue :param worker_id: Worker ID :param results: A list of tuples of job ID, success bool, and output from the worker """ if worker_id is not None: self._results_added[worker_id] += 1 self._results_queue.put((worker_id, results)) def get_results(self, block: bool = True, timeout: Optional[float] = None) -> Any: """ Obtain the next result from the results queue :param block: Whether to block (wait for results) :param timeout: How long to wait for results in case ``block==True`` :return: The next result from the queue, which is the result of calling the function """ try: with DelayedKeyboardInterrupt(): worker_id, results = self._results_queue.get(block=block, timeout=timeout) self._results_queue.task_done() if worker_id is not None: with self._results_received.get_lock(): self._results_received[worker_id] += 1 self._last_completed_task_worker_id.append(worker_id) return results except EOFError: # This can occur when an imap function was running, while at the same time terminate() was called return [(None, None, POISON_PILL)] def reset_results_received(self, worker_id: int) -> None: """ Reset the number of results received from a worker :param worker_id: Worker ID """ self._results_received[worker_id] = 0 def wait_for_all_results_received(self, worker_id: int) -> None: """ Wait for the main process to receive all the results from a specific worker :param worker_id: Worker ID """ while self._results_received[worker_id] != self._results_added[worker_id]: time.sleep(0.01) def add_new_map_params(self, map_params: WorkerMapParams) -> None: """ Submits new map params for each worker :param map_params: New map params """ for worker_id in range(self.n_jobs): self.add_task(None, NEW_MAP_PARAMS_PILL, worker_id) self.add_task(None, map_params, worker_id) ################ # Exceptions ################ def signal_exception_thrown(self, job_id: int) -> None: """ Set the exception event :param job_id: Job ID which triggered the exception """ self._exception_job_id.value = job_id self._exception_thrown.set() def exception_thrown(self) -> bool: """ :return: Whether an exception was thrown by one of the workers """ return self._exception_thrown.is_set() def wait_for_exception_thrown(self, timeout: Optional[float]) -> bool: """ Waits until the exception thrown event is set :param timeout: How long to wait before giving up :return: True when exception was thrown, False if timeout was reached """ return self._exception_thrown.wait(timeout=timeout) def get_exception_thrown_job_id(self) -> int: """ :return: Job ID which triggered the exception """ return self._exception_job_id.value def signal_kill_signal_received(self) -> None: """ Set the kill signal received event """ self._kill_signal_received.value = True def kill_signal_received(self) -> bool: """ :return: Whether a kill signal was received in one of the workers """ return self._kill_signal_received.value ################ # Terminating & restarting ################ def insert_poison_pill(self) -> None: """ 'Tell' the workers their job is done. """ for worker_id in range(self.n_jobs): self.add_task(None, POISON_PILL, worker_id) def insert_poison_pill_results_listener(self) -> None: """ 'Tell' the apply results listener their job is done. """ self.add_results(None, [(None, True, POISON_PILL)]) def insert_non_lethal_poison_pill(self) -> None: """ When ``keep_alive=True``, the workers should stay alive, but they need to wrap up their work (like sending the latest progress bar update) """ for worker_id in range(self.n_jobs): self.add_task(None, NON_LETHAL_POISON_PILL, worker_id) def signal_worker_restart(self, worker_id: int) -> None: """ Signal to the main process that this worker needs to be restarted :param worker_id: Worker ID """ self._worker_restart_array[worker_id] = True with self._worker_restart_condition: self._worker_restart_condition.notify() def signal_worker_restart_condition(self) -> None: """ Signal the condition primitive, such that the worker restart handler thread can continue. This is useful when an exception has been thrown and the thread needs to exit. """ with self._worker_restart_condition: self._worker_restart_condition.notify() def get_worker_restarts(self) -> List[int]: """ Obtain the worker IDs that need to be restarted. Blocks until at least one worker needs to be restarted. It returns an empty list when an exception has been thrown (which also notifies the worker_done_condition) :return: List of worker IDs """ def _get_worker_restarts(): return [worker_id for worker_id, restart in enumerate(self._worker_restart_array) if restart] with self._worker_restart_condition: # If there aren't any workers to restart, wait until there are worker_ids = _get_worker_restarts() if not worker_ids: self._worker_restart_condition.wait() worker_ids = _get_worker_restarts() return worker_ids def reset_worker_restart(self, worker_id) -> None: """ Worker has been restarted, reset signal. :param worker_id: Worker ID """ self._worker_restart_array[worker_id] = False def signal_worker_alive(self, worker_id: int) -> None: """ Indicate that a worker is alive :param worker_id: Worker ID """ self._workers_dead[worker_id] = False def signal_worker_dead(self, worker_id: int) -> None: """ ` Indicate that a worker is dead :param worker_id: Worker ID """ self._workers_dead[worker_id] = True def is_worker_alive(self, worker_id: int) -> bool: """ Check whether the worker is alive :param worker_id: Worker ID :return: Whether the worker is alive """ return not self._workers_dead[worker_id] def join_results_queues(self, keep_alive: bool = False) -> None: """ Join results and exit results queues :param keep_alive: Whether to keep the queues alive """ self._results_queue.join() if not keep_alive: self._results_queue.close() self._results_queue.join_thread() def join_task_queues(self, keep_alive: bool = False) -> None: """ Join task queues :param keep_alive: Whether to keep the queues alive """ [q.join() for q in self._task_queues] if not keep_alive: [q.close() for q in self._task_queues] [q.join_thread() for q in self._task_queues] def drain_results_queue_terminate_worker(self, dont_wait_event: threading.Event) -> None: """ Drain the results queue without blocking. This is done when terminating workers, while they could still be busy putting something in the queues. This function will always be called from within a thread. :param dont_wait_event: Event object to indicate whether other termination threads should continue. I.e., when we set it to False, threads should wait. """ # Get results from the results queue. If we got any, keep going and inform the other termination threads to wait # until this one's finished got_results = False try: while True: self.get_results(block=False) dont_wait_event.clear() got_results = True except (queue.Empty, OSError): if got_results: dont_wait_event.set() def drain_queues(self) -> None: """ Drain tasks and results queues """ [self.drain_and_join_queue(q) for q in self._task_queues] self.drain_and_join_queue(self._results_queue) def drain_and_join_queue(self, q: mp.JoinableQueue, join: bool = True) -> None: """ Drains a queue completely, such that it is joinable. If a timeout is reached, we give up and terminate. So far, I've only seen it happen when an exception is thrown when using spawn as start method, and even then it only happens once every 1000 runs or so. :param q: Queue to join :param join: Whether to join the queue or not """ # Running this in a separate process on Windows can cause errors if RUNNING_WINDOWS: self._drain_and_join_queue(q, join) else: try: process = self.ctx.Process(target=self._drain_and_join_queue, args=(q, join)) process.start() process.join(timeout=5) if process.is_alive(): process.terminate() process.join() if join: # The above was done in a separate process where the queue had a different feeder thread q.close() q.join_thread() except OSError: # Queue could be just closed when starting the drain_and_join_queue process pass @staticmethod def _drain_and_join_queue(q: mp.JoinableQueue, join: bool = True) -> None: """ Drains a queue completely, such that it is joinable :param q: Queue to join :param join: Whether to join the queue or not """ # Do nothing when it's not set if q is None: return # Call task done up to the point where we get a ValueError. We need to do this when child processes already # started processing on some tasks and got terminated half-way. n = 0 try: while True: q.task_done() n += 1 except (OSError, ValueError): pass try: while not q.empty() or n != 0: q.get(block=True, timeout=1.0) n -= 1 except (OSError, EOFError, queue.Empty, ValueError): pass # Join if join: try: q.join() q.close() q.join_thread() except (OSError, ValueError): pass ################ # Timeouts ################ def signal_worker_init_started(self, worker_id: int) -> None: """ Sets the worker_init started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3] = time.time() def signal_worker_task_started(self, worker_id: int) -> None: """ Sets the task started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3 + 1] = time.time() def signal_worker_exit_started(self, worker_id: int) -> None: """ Sets the worker_exit started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3 + 2] = time.time() def signal_worker_init_completed(self, worker_id: int) -> None: """ Resets the worker_init started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3] = 0 def signal_worker_task_completed(self, worker_id: int) -> None: """ Resets the task started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3 + 1] = 0 def signal_worker_exit_completed(self, worker_id: int) -> None: """ Resets the worker_exit started timestamp for a specific worker :param worker_id: Worker ID """ self._workers_time_task_started[worker_id * 3 + 2] = 0 def has_worker_init_timed_out(self, worker_id: int, timeout: float) -> bool: """ Checks whether a worker_init takes longer than the timeout value :param worker_id: Worker ID :param timeout: Timeout in seconds :return: True when time has expired, False otherwise """ started_time = self._workers_time_task_started[worker_id * 3] return self._has_worker_timed_out(started_time, timeout) def has_worker_task_timed_out(self, worker_id: int, timeout: float) -> bool: """ Checks whether a worker task takes longer than the timeout value :param worker_id: Worker ID :param timeout: Timeout in seconds :return: True when time has expired, False otherwise """ started_time = self._workers_time_task_started[worker_id * 3 + 1] return self._has_worker_timed_out(started_time, timeout) def has_worker_exit_timed_out(self, worker_id: int, timeout: float) -> bool: """ Checks whether a worker_exit takes longer than the timeout value :param worker_id: Worker ID :param timeout: Timeout in seconds :return: True when time has expired, False otherwise """ started_time = self._workers_time_task_started[worker_id * 3 + 2] return self._has_worker_timed_out(started_time, timeout) @staticmethod def _has_worker_timed_out(started_time: float, timeout: float) -> bool: """ Checks whether time has passed beyond the timeout :param started_time: Timestamp :param timeout: Timeout in seconds :return: True when time has expired, False otherwise """ return False if started_time == 0.0 else (time.time() - started_time) >= timeout mpire-2.10.2/mpire/context.py000066400000000000000000000033011461637447300160760ustar00rootroot00000000000000import multiprocessing as mp try: import multiprocess as mp_dill import multiprocess.managers # Needed in utils.py except ImportError: mp_dill = None import platform import threading # Check if fork is available as start method. It's not available on Windows machines try: mp.get_context('fork') FORK_AVAILABLE = True except ValueError: FORK_AVAILABLE = False # Check if we're running on Windows or MacOS RUNNING_WINDOWS = platform.system() == "Windows" RUNNING_MACOS = platform.system() == "Darwin" # Threading context so we can use threading as backend as well class ThreadingContext: Barrier = threading.Barrier Condition = threading.Condition Event = threading.Event Lock = threading.Lock RLock = threading.RLock Thread = threading.Thread # threading doesn't have Array and JoinableQueue, so we take it from multiprocessing. Both are thread-safe. We need # the Process class for the MPIRE insights SyncManager instance. Array = mp.Array JoinableQueue = mp.JoinableQueue Process = mp.Process Value = mp.Value MP_CONTEXTS = {'mp': {'fork': mp.get_context('fork') if FORK_AVAILABLE else None, 'forkserver': mp.get_context('forkserver') if FORK_AVAILABLE else None, 'spawn': mp.get_context('spawn')}, 'threading': ThreadingContext} if mp_dill is not None: MP_CONTEXTS['mp_dill'] = {'fork': mp_dill.get_context('fork') if FORK_AVAILABLE else None, 'forkserver': mp_dill.get_context('forkserver') if FORK_AVAILABLE else None, 'spawn': mp_dill.get_context('spawn')} DEFAULT_START_METHOD = 'fork' if FORK_AVAILABLE else 'spawn' mpire-2.10.2/mpire/dashboard/000077500000000000000000000000001461637447300157725ustar00rootroot00000000000000mpire-2.10.2/mpire/dashboard/__init__.py000066400000000000000000000007461461637447300201120ustar00rootroot00000000000000try: from mpire.dashboard.dashboard import connect_to_dashboard, shutdown_dashboard, start_dashboard from mpire.dashboard.utils import get_stacklevel, set_stacklevel except (ImportError, ModuleNotFoundError): def _not_installed(*_, **__): raise NotImplementedError("Install the dashboard dependencies to enable the dashboard") connect_to_dashboard = shutdown_dashboard = start_dashboard = _not_installed get_stacklevel = set_stacklevel = _not_installed mpire-2.10.2/mpire/dashboard/connection_classes.py000066400000000000000000000020431461637447300222170ustar00rootroot00000000000000from dataclasses import dataclass from multiprocessing import Event from multiprocessing.managers import BaseManager from multiprocessing.synchronize import Event as EventType from typing import Optional class DashboardStartedEvent: def __init__(self) -> None: self.event: Optional[EventType] = None def init(self) -> None: self.event = Event() def reset(self) -> None: self.event = None def set(self) -> None: if self.event is None: self.init() self.event.set() def is_set(self) -> bool: return self.event.is_set() if self.event is not None else False def wait(self, timeout: Optional[float] = None) -> bool: return self.event.wait(timeout) if self.event is not None else False class DashboardManager(BaseManager): pass @dataclass class DashboardManagerConnectionDetails: host: Optional[str] = None port: Optional[int] = None def clear(self) -> None: self.host = None self.port = None mpire-2.10.2/mpire/dashboard/connection_utils.py000066400000000000000000000045301461637447300217250ustar00rootroot00000000000000from typing import Optional, Tuple from mpire.dashboard.connection_classes import DashboardManagerConnectionDetails, DashboardStartedEvent # If a user has not installed the dashboard dependencies than the imports below will fail try: from mpire.dashboard import connect_to_dashboard from mpire.dashboard.dashboard import DASHBOARD_STARTED_EVENT from mpire.dashboard.manager import DASHBOARD_MANAGER_CONNECTION_DETAILS except (ImportError, ModuleNotFoundError): DASHBOARD_MANAGER_CONNECTION_DETAILS = DashboardManagerConnectionDetails() DASHBOARD_STARTED_EVENT = DashboardStartedEvent() def connect_to_dashboard(*_): pass DashboardConnectionDetails = Tuple[Optional[str], Optional[int], bool] def get_dashboard_connection_details() -> DashboardConnectionDetails: """ Obtains the connection details of a dasbhoard. These details are needed to be passed on to child process when the start method is either forkserver or spawn. :return: Dashboard manager host, port_nr and whether a dashboard is started/connected """ return (DASHBOARD_MANAGER_CONNECTION_DETAILS.host, DASHBOARD_MANAGER_CONNECTION_DETAILS.port, DASHBOARD_STARTED_EVENT.is_set()) def set_dashboard_connection(dashboard_connection_details: DashboardConnectionDetails, auto_connect: bool = True) -> None: """ Sets the dashboard connection details and connects to an existing dashboard if needed. :param dashboard_connection_details: Dashboard manager host, port_nr and whether a dashboard is started/connected :param auto_connect: Whether to automatically connect to a server when the dashboard_started event is set """ global DASHBOARD_MANAGER_CONNECTION_DETAILS dashboard_manager_host, dashboard_manager_port_nr, dashboard_started = dashboard_connection_details if (dashboard_manager_host is not None and dashboard_manager_port_nr is not None and not DASHBOARD_STARTED_EVENT.is_set()): if dashboard_started and auto_connect: connect_to_dashboard(dashboard_manager_port_nr, dashboard_manager_host) else: DASHBOARD_MANAGER_CONNECTION_DETAILS.host = dashboard_manager_host DASHBOARD_MANAGER_CONNECTION_DETAILS.port = dashboard_manager_port_nr if dashboard_started: DASHBOARD_STARTED_EVENT.set() mpire-2.10.2/mpire/dashboard/dashboard.py000066400000000000000000000214121461637447300202730ustar00rootroot00000000000000import atexit import getpass try: from importlib.resources import files as resource except ImportError: # Python < 3.9 compatibility from importlib_resources import files as resource import logging import os import signal import socket from datetime import datetime from multiprocessing import Event, Process from multiprocessing.managers import BaseProxy from typing import Dict, Optional, Sequence, Tuple, Union from flask import Flask, jsonify, render_template, request from markupsafe import escape from werkzeug.serving import make_server from mpire.dashboard.connection_classes import DashboardStartedEvent from mpire.dashboard.manager import (DASHBOARD_MANAGER_CONNECTION_DETAILS, get_manager_client_dicts, shutdown_manager_server, start_manager_server) from mpire.dashboard.utils import get_two_available_ports logger = logging.getLogger(__name__) logger_werkzeug = logging.getLogger('werkzeug') logger_werkzeug.setLevel(logging.ERROR) app = Flask(__name__) _server_process = None with open(resource('mpire.dashboard') / 'templates' / 'progress_bar.html', 'r') as fp: _progress_bar_html = fp.read() _DASHBOARD_MANAGER = None _DASHBOARD_TQDM_DICT = None _DASHBOARD_TQDM_DETAILS_DICT = None DASHBOARD_STARTED_EVENT = DashboardStartedEvent() @app.route('/') def index() -> str: """ Obtain the index HTML :return: HTML """ # Obtain user. This can fail when the current uid refers to a non-existing user, which can happen when running in a # container as a non-root user. See https://github.com/sybrenjansen/mpire/issues/128. try: user = getpass.getuser() except KeyError: user = "n/a" return render_template('index.html', username=user, hostname=socket.gethostname(), manager_host=DASHBOARD_MANAGER_CONNECTION_DETAILS.host or 'localhost', manager_port_nr=DASHBOARD_MANAGER_CONNECTION_DETAILS.port) @app.route('/_progress_bar_update') def progress_bar_update() -> str: """ Obtain progress bar updates (should be called through AJAX) :return: JSON string containing progress bar updates """ # As we get updates only when the progress bar is updated we need to fix the 'duration' and 'time remaining' parts # (time never stops) now = datetime.now() result = [] for pb_id in sorted(_DASHBOARD_TQDM_DICT.keys()): progress = _DASHBOARD_TQDM_DICT.get(pb_id) if progress['total'] is None: progress['total'] = '?' if progress['success'] and progress['n'] != progress['total']: progress['duration'] = str(now - progress['started_raw']).rsplit('.', 1)[0] progress['remaining'] = (str(progress['finished_raw'] - now).rsplit('.', 1)[0] if progress['finished_raw'] is not None and progress['finished_raw'] > now else '-') result.append(progress) return jsonify(result=result) @app.route('/_progress_bar_new') def progress_bar_new() -> str: """ Obtain a piece of HTML for a new progress bar (should be called through AJAX) :return: JSON string containing new progress bar HTML """ pb_id = int(request.args['pb_id']) has_insights = request.args['has_insights'] == 'true' # Obtain progress bar details. Only show the user@host part if it doesn't equal the user@host of this process # (in case someone connected to this dashboard from another machine or user) progress_bar_details = _DASHBOARD_TQDM_DETAILS_DICT.get(pb_id) if progress_bar_details['user'] == f'{getpass.getuser()}@{socket.gethostname()}': progress_bar_details['user'] = '' else: progress_bar_details['user'] = '{}:'.format(progress_bar_details['user']) # Create table for worker insights insights_workers = [] if has_insights: for worker_id in range(progress_bar_details['n_jobs']): insights_workers.append(f"{worker_id}" f"" f"" f"" f"" f"" f"" f"") insights_workers = "\n".join(insights_workers) return jsonify(result=_progress_bar_html.format(id=pb_id, insights_workers=insights_workers, has_insights='block' if has_insights else 'none', **{k: escape(v) for k, v in progress_bar_details.items()})) def start_dashboard(port_range: Sequence = range(8080, 8100)) -> Dict[str, Union[int, str]]: """ Starts a new MPIRE dashboard :param port_range: Port range to try. :return: A dictionary containing the dashboard port number and manager host and port number being used """ global _server_process, _DASHBOARD_MANAGER if not DASHBOARD_STARTED_EVENT.is_set(): DASHBOARD_STARTED_EVENT.init() dashboard_port_nr, manager_port_nr = get_two_available_ports(port_range) # Set up manager server _DASHBOARD_MANAGER = start_manager_server(manager_port_nr) # Start flask server logging.getLogger('werkzeug').setLevel(logging.WARN) _server_process = Process(target=_run, args=(DASHBOARD_STARTED_EVENT, dashboard_port_nr, get_manager_client_dicts()), daemon=True, name='dashboard-process') _server_process.start() DASHBOARD_STARTED_EVENT.wait() # Return connect information return {'dashboard_port_nr': dashboard_port_nr, 'manager_host': DASHBOARD_MANAGER_CONNECTION_DETAILS.host or socket.gethostname(), 'manager_port_nr': DASHBOARD_MANAGER_CONNECTION_DETAILS.port} else: raise RuntimeError("You already have a running dashboard") @atexit.register def shutdown_dashboard() -> None: """ Shuts down the dashboard """ if DASHBOARD_STARTED_EVENT.is_set(): global _server_process, _DASHBOARD_MANAGER, _DASHBOARD_TQDM_DICT, _DASHBOARD_TQDM_DETAILS_DICT if _server_process is not None: # Send SIGINT to the server process, which is the only way to stop it without causing semaphore leaks os.kill(_server_process.pid, signal.SIGINT) _server_process.join() shutdown_manager_server(_DASHBOARD_MANAGER) _DASHBOARD_MANAGER = None _DASHBOARD_TQDM_DICT = None _DASHBOARD_TQDM_DETAILS_DICT = None DASHBOARD_STARTED_EVENT.reset() def connect_to_dashboard(manager_port_nr: int, manager_host: Optional[Union[bytes, str]] = None) -> None: """ Connects to an existing MPIRE dashboard :param manager_port_nr: Port to use when connecting to a manager :param manager_host: Host to use when connecting to a manager. If ``None`` it will use localhost """ global _DASHBOARD_MANAGER, DASHBOARD_MANAGER_CONNECTION_DETAILS if DASHBOARD_STARTED_EVENT.is_set(): raise RuntimeError("You're already connected to a running dashboard") # Set connection variables so we can connect to the right manager manager_host = manager_host or "127.0.0.1" DASHBOARD_MANAGER_CONNECTION_DETAILS.host = manager_host DASHBOARD_MANAGER_CONNECTION_DETAILS.port = manager_port_nr # Try to connect try: get_manager_client_dicts() except ConnectionRefusedError: raise ConnectionRefusedError("Could not connect to dashboard manager at " f"{manager_host.decode()}:{manager_port_nr}") DASHBOARD_STARTED_EVENT.set() def _run(started: Event, dashboard_port_nr: int, manager_client_dicts: Tuple[BaseProxy, BaseProxy, BaseProxy]) -> None: """ Starts a dashboard server :param started: Event that signals the dashboard server has started :param manager_host: Dashboard manager host :param manager_port_nr: Dashboard manager port number :param dashboard_port_nr: Dashboard port number """ global _DASHBOARD_TQDM_DICT, _DASHBOARD_TQDM_DETAILS_DICT _DASHBOARD_TQDM_DICT, _DASHBOARD_TQDM_DETAILS_DICT, _ = manager_client_dicts # Start server server = make_server('0.0.0.0', dashboard_port_nr, app) started.set() logger.info(f"Server started on 0.0.0.0:{dashboard_port_nr}") server.serve_forever() mpire-2.10.2/mpire/dashboard/manager.py000066400000000000000000000105731461637447300177640ustar00rootroot00000000000000from multiprocessing import Lock from multiprocessing.synchronize import Lock as LockType from multiprocessing.managers import BaseProxy from typing import Dict, Optional, Tuple from mpire.dashboard.connection_classes import DashboardManager, DashboardManagerConnectionDetails from mpire.signal import ignore_keyboard_interrupt # Dict for tqdm progress bar updates DASHBOARD_TQDM_DICT = None # Dict for tqdm progress bar details (function called etc.) DASHBOARD_TQDM_DETAILS_DICT = None # Lock for registering new progress bars DASHBOARD_TQDM_LOCK = None # Connection details for connecting to a manager DASHBOARD_MANAGER_CONNECTION_DETAILS = DashboardManagerConnectionDetails() def get_dashboard_tqdm_dict() -> Dict: """ :return: Dashboard tqdm dict which should be used in a DashboardManager context """ global DASHBOARD_TQDM_DICT if DASHBOARD_TQDM_DICT is None: DASHBOARD_TQDM_DICT = {} return DASHBOARD_TQDM_DICT def get_dashboard_tqdm_details_dict() -> Dict: """ :return: Dashboard tqdm details dict which should be used in a DashboardManager context """ global DASHBOARD_TQDM_DETAILS_DICT if DASHBOARD_TQDM_DETAILS_DICT is None: DASHBOARD_TQDM_DETAILS_DICT = {} return DASHBOARD_TQDM_DETAILS_DICT def get_dashboard_tqdm_lock() -> LockType: """ :return: Dashboard tqdm lock which should be used in a DashboardManager context """ global DASHBOARD_TQDM_LOCK if DASHBOARD_TQDM_LOCK is None: DASHBOARD_TQDM_LOCK = Lock() return DASHBOARD_TQDM_LOCK def start_manager_server(manager_port_nr: int) -> DashboardManager: """ Start a SyncManager :param manager_port_nr: Port number to use for the manager :return: SyncManager and hostname """ global DASHBOARD_TQDM_DICT, DASHBOARD_TQDM_DETAILS_DICT, DASHBOARD_TQDM_LOCK, \ DASHBOARD_MANAGER_HOST, DASHBOARD_MANAGER_PORT DashboardManager.register('get_dashboard_tqdm_dict', get_dashboard_tqdm_dict) DashboardManager.register('get_dashboard_tqdm_details_dict', get_dashboard_tqdm_details_dict) DashboardManager.register('get_dashboard_tqdm_lock', get_dashboard_tqdm_lock) # Create manager dm = DashboardManager(address=("127.0.0.1", manager_port_nr), authkey=b'mpire_dashboard') dm.start(ignore_keyboard_interrupt) DASHBOARD_TQDM_DICT = dm.get_dashboard_tqdm_dict() DASHBOARD_TQDM_DETAILS_DICT = dm.get_dashboard_tqdm_details_dict() DASHBOARD_TQDM_LOCK = dm.get_dashboard_tqdm_lock() # Set host and port number so other processes know where to connect to DASHBOARD_MANAGER_CONNECTION_DETAILS.host = "127.0.0.1" DASHBOARD_MANAGER_CONNECTION_DETAILS.port = manager_port_nr return dm def shutdown_manager_server(manager: Optional[DashboardManager]) -> None: """ Shutdown a DashboardManager :param manager: DashboardManager to shutdown """ global DASHBOARD_TQDM_DICT, DASHBOARD_TQDM_DETAILS_DICT, DASHBOARD_TQDM_LOCK if manager is not None: manager.shutdown() DASHBOARD_TQDM_DICT = None DASHBOARD_TQDM_DETAILS_DICT = None DASHBOARD_TQDM_LOCK = None DASHBOARD_MANAGER_CONNECTION_DETAILS.clear() def get_manager_client_dicts() -> Tuple[BaseProxy, BaseProxy, BaseProxy]: """ Connect to a DashboardManager and obtain the synchronized tqdm dashboard dicts :return: DashboardManager tqdm dict, tqdm details dict, tqdm lock """ global DASHBOARD_TQDM_DICT, DASHBOARD_TQDM_DETAILS_DICT, DASHBOARD_TQDM_LOCK # If we're already connected to a manager, return the dicts directly if DASHBOARD_TQDM_DICT is not None: return DASHBOARD_TQDM_DICT, DASHBOARD_TQDM_DETAILS_DICT, DASHBOARD_TQDM_LOCK # Connect to a server DashboardManager.register('get_dashboard_tqdm_dict', get_dashboard_tqdm_dict) DashboardManager.register('get_dashboard_tqdm_details_dict', get_dashboard_tqdm_details_dict) DashboardManager.register('get_dashboard_tqdm_lock', get_dashboard_tqdm_lock) dm = DashboardManager( address=(DASHBOARD_MANAGER_CONNECTION_DETAILS.host, DASHBOARD_MANAGER_CONNECTION_DETAILS.port), authkey=b'mpire_dashboard' ) dm.connect() DASHBOARD_TQDM_DICT = dm.get_dashboard_tqdm_dict() DASHBOARD_TQDM_DETAILS_DICT = dm.get_dashboard_tqdm_details_dict() DASHBOARD_TQDM_LOCK = dm.get_dashboard_tqdm_lock() return DASHBOARD_TQDM_DICT, DASHBOARD_TQDM_DETAILS_DICT, DASHBOARD_TQDM_LOCK mpire-2.10.2/mpire/dashboard/static/000077500000000000000000000000001461637447300172615ustar00rootroot00000000000000mpire-2.10.2/mpire/dashboard/static/bootstrap.bundle.min.js000066400000000000000000002314531461637447300236760ustar00rootroot00000000000000/*! * Bootstrap v4.3.1 (https://getbootstrap.com/) * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery")):"function"==typeof define&&define.amd?define(["exports","jquery"],e):e((t=t||self).bootstrap={},t.jQuery)}(this,function(t,p){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)p(this._element).one(q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n=i.clientWidth&&n>=i.clientHeight}),h=0l[t]&&!i.escapeWithReference&&(n=Math.min(h[e],l[t]-("right"===t?h.width:h.height))),Kt({},e,n)}};return c.forEach(function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";h=Qt({},h,u[e](t))}),t.offsets.popper=h,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!fe(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",h=l?"Top":"Left",u=h.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=Zt(i)[c];a[d]-ps[d]&&(t.offsets.popper[u]+=a[u]+p-s[d]),t.offsets.popper=Vt(t.offsets.popper);var m=a[u]+a[c]/2-p/2,g=Nt(t.instance.popper),_=parseFloat(g["margin"+h],10),v=parseFloat(g["border"+h+"Width"],10),y=m-t.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),t.arrowElement=i,t.offsets.arrow=(Kt(n={},u,Math.round(y)),Kt(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(p,m){if(oe(p.instance.modifiers,"inner"))return p;if(p.flipped&&p.placement===p.originalPlacement)return p;var g=Gt(p.instance.popper,p.instance.reference,m.padding,m.boundariesElement,p.positionFixed),_=p.placement.split("-")[0],v=te(_),y=p.placement.split("-")[1]||"",E=[];switch(m.behavior){case ge:E=[_,v];break;case _e:E=me(_);break;case ve:E=me(_,!0);break;default:E=m.behavior}return E.forEach(function(t,e){if(_!==t||E.length===e+1)return p;_=p.placement.split("-")[0],v=te(_);var n,i=p.offsets.popper,o=p.offsets.reference,r=Math.floor,s="left"===_&&r(i.right)>r(o.left)||"right"===_&&r(i.left)r(o.top)||"bottom"===_&&r(i.top)r(g.right),c=r(i.top)r(g.bottom),u="left"===_&&a||"right"===_&&l||"top"===_&&c||"bottom"===_&&h,f=-1!==["top","bottom"].indexOf(_),d=!!m.flipVariations&&(f&&"start"===y&&a||f&&"end"===y&&l||!f&&"start"===y&&c||!f&&"end"===y&&h);(s||u||d)&&(p.flipped=!0,(s||u)&&(_=E[e+1]),d&&(y="end"===(n=y)?"start":"start"===n?"end":n),p.placement=_+(y?"-"+y:""),p.offsets.popper=Qt({},p.offsets.popper,ee(p.instance.popper,p.offsets.reference,p.placement)),p=ie(p.instance.modifiers,p,"flip"))}),p},behavior:"flip",padding:5,boundariesElement:"viewport"},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=te(e),t.offsets.popper=Vt(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!fe(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=ne(t.instance.modifiers,function(t){return"preventOverflow"===t.name}).boundaries;if(e.bottomn.right||e.top>n.bottom||e.rightdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:vn},Ln="show",xn="out",Pn={HIDE:"hide"+Tn,HIDDEN:"hidden"+Tn,SHOW:"show"+Tn,SHOWN:"shown"+Tn,INSERTED:"inserted"+Tn,CLICK:"click"+Tn,FOCUSIN:"focusin"+Tn,FOCUSOUT:"focusout"+Tn,MOUSEENTER:"mouseenter"+Tn,MOUSELEAVE:"mouseleave"+Tn},Hn="fade",jn="show",Rn=".tooltip-inner",Fn=".arrow",Mn="hover",Wn="focus",Un="click",Bn="manual",qn=function(){function i(t,e){if("undefined"==typeof be)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(jn))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===p(this.element).css("display"))throw new Error("Please use show on visible elements");var t=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(t);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&p(o).addClass(Hn);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new be(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Fn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),p(o).addClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().on("mouseover",null,p.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,p(e.element).trigger(e.constructor.Event.SHOWN),t===xn&&e._leave(null,e)};if(p(this.tip).hasClass(Hn)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=p.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Ln&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p(this.element).trigger(i),!i.isDefaultPrevented()){if(p(n).removeClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),this._activeTrigger[Un]=!1,this._activeTrigger[Wn]=!1,this._activeTrigger[Mn]=!1,p(this.tip).hasClass(Hn)){var r=m.getTransitionDurationFromElement(n);p(n).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){p(this.getTipElement()).addClass(Dn+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(p(t.querySelectorAll(Rn)),this.getTitle()),p(t).removeClass(Hn+" "+jn)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=bn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p(e).parent().is(t)||t.empty().append(e):t.text(p(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},t._getAttachment=function(t){return Nn[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Bn){var e=t===Mn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Mn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),p(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Wn:Mn]=!0),p(e.getTipElement()).hasClass(jn)||e._hoverState===Ln?e._hoverState=Ln:(clearTimeout(e._timeout),e._hoverState=Ln,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ln&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Wn:Mn]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=xn,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===xn&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=p(this.element).data();return Object.keys(e).forEach(function(t){-1!==An.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),m.typeCheckConfig(wn,t,this.constructor.DefaultType),t.sanitize&&(t.template=bn(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(In);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p(t).removeClass(Hn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(Cn),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(Cn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return kn}},{key:"NAME",get:function(){return wn}},{key:"DATA_KEY",get:function(){return Cn}},{key:"Event",get:function(){return Pn}},{key:"EVENT_KEY",get:function(){return Tn}},{key:"DefaultType",get:function(){return On}}]),i}();p.fn[wn]=qn._jQueryInterface,p.fn[wn].Constructor=qn,p.fn[wn].noConflict=function(){return p.fn[wn]=Sn,qn._jQueryInterface};var Kn="popover",Qn="bs.popover",Vn="."+Qn,Yn=p.fn[Kn],zn="bs-popover",Xn=new RegExp("(^|\\s)"+zn+"\\S+","g"),Gn=l({},qn.Default,{placement:"right",trigger:"click",content:"",template:''}),$n=l({},qn.DefaultType,{content:"(string|element|function)"}),Jn="fade",Zn="show",ti=".popover-header",ei=".popover-body",ni={HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn,INSERTED:"inserted"+Vn,CLICK:"click"+Vn,FOCUSIN:"focusin"+Vn,FOCUSOUT:"focusout"+Vn,MOUSEENTER:"mouseenter"+Vn,MOUSELEAVE:"mouseleave"+Vn},ii=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){p(this.getTipElement()).addClass(zn+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},o.setContent=function(){var t=p(this.getTipElement());this.setElementContent(t.find(ti),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ei),e),t.removeClass(Jn+" "+Zn)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(Xn);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||tcode{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .4s}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} /*# sourceMappingURL=bootstrap.min.css.map */mpire-2.10.2/mpire/dashboard/static/fonts/000077500000000000000000000000001461637447300204125ustar00rootroot00000000000000mpire-2.10.2/mpire/dashboard/static/fonts/glyphicons-halflings-regular.eot000066400000000000000000000472371461637447300267230ustar00rootroot00000000000000NAMLP',(GLYPHICONS HalflingsRegularxVersion 1.009;PS 001.009;hotconv 1.0.70;makeotf.lib2.5.583298GLYPHICONS Halflings RegularBSGPMMF٣(uʌ<0DB/X N CC^ rmR2skPJ"5+glW*iW/E4#ԣU~fUDĹJ1/!/s7k(hN8od$yq19@-HGS"Fjؠ6C3&W51BaQaRU/{*=@dh$1Tۗnc+cA Zɀ@Qcal2>Km' CHMĬfBX,Ype U*Ҕz miO1nE. hx!aC XTV‹ R%|IHP5"bN=r/_R_ %҄uzҘ52ġP)F7SqF{nia@Ds;}9⬥?ź R{Tk;޵ǜU\NZQ-^s7f 0S3A _n`W7Ppi!g/_pZ-=ץ~WZ#/4 KF` z0| Dѵ&däIÏ;M{'omm I !wi9|H:ۧ{~qO, L]&J09/9&Y 蓰{;'3`e@vHyDZ$3Dx28 W Cx5xwB`$C$'ElyhԀ DJ $(pQAA܉A@'$ hp0V0 `se$4$"t2=f4A{Tk0|rH`L&sh]A<`R'!1N;_t3# V *veF`E O${)W=p:F`22ړC^.ćG<.pNe2ִ+Ysl:˼ ܫu5tu^86ȄTmyQ%u~%~1rҘawߚ^_ZZa0!N`. uqYB\ᨀ[e:@J'Eہ,3ubj@pfeW9( ޅ=lG7gj SM609OˑlBa݁ <Bՙ(VRApf^+g9qMt]تpEr@]@VkV ud^X R@?EY2]#Ǽ4JK'dPC|mmn#$+48u'e&[n[L%{BCDL:^!bƙ:&g3-3ub iLZڂWFSId6.k5Pl77UzT:NN.")['|U"AIvwptdk9嫫9nDmq7I|6Kbc]MBABȪ_JT q  6@Fhd`GT:M7'L,IhFP ~j $¡„ 3hA-S^چ-%qe~Qqln"i&Qe?FlK"As(3Y;"Let'RzM1 0{=) K%$C 9M4c EotjVGD)l8,\w !%$3t TBzҴ iUJ[xgdBr$!eq"J> )\~3(^ R€8#>bHG'7_ fӫcκtDoAA߃(qB<``VΫ֘*buP4v@+.Qԥ$V@C0 RܐP[z:XH#e s>?EWO>@I$|si ES)0A?9ab,@K̩o&Q% ϞLu+ +H|Ɛ?NK4CnPt 'OT.j5Ĵ8vw֜I&+`yScaO[#gQd[KI矗`ČLP # )27aTi@c\ސ 0nCpߖ運4͵x*RzYbT[\kUvHʈqp঄IIŗ) bB XPNtz 2 I== ;}bqjiކa#" >11Ap1POOuxQ Fϲ(h݄O'MDxLK$ȵh& 14SirHJPtDM;rM+ *ؗ5u2$f3K %ѳb (@,2f,~"7R;E;HX(42Z'Tۿ2J+^!#oY~4-׃GW*!A0&8f{`W=DP8'= R g}iP>#4EBRY^4eN8V,[BĨD#X],LBsNC> +o^x jC.4Ya_{eA2=r+9POA!! }YPJeGn%x1/}RgHa ^3- 5 |qSaWK{ 1al`I1 Qf_yyCZ)L3X] W6@DMT<.uGK8DsбWr\7Z\V"ISd>CUjeD 3MtWcPӉ6#3QnቩJ\7#磱`؀K lV6 &T ~l. @61`! ` wYk/a0A¹ԁYhdxk:fEao̟^<IwYgq7s[ -y1ع5aMKאRBYFq}8*Nt'.YbZvK (]&ɜ(ՙ2:0 oΏхPKiBH4UX,[$ 0mXش f50VR 8%ާDtUs`-BPzPsvI8z-t1DiB "˶YTJ .?07jLN[2tĮ̎ #6?E׻:ɞY;A&qSIR)ss 9*x0Bj)mHAhyЏhMm&4Ŋ4 gV&tYOCS0Yd7MvNj)wA(o "͢[ E`7ezď-Q]6+Bca@^I:һ=sSnc 6 OB4LGpBq/>O pwj A*@JC[h&3B Qbϩ8 :%f~v/lS00a"B8(f uGoǚgyt_y~͔ %mL !I$Xt0~ePz]Ug Н=_?.j#+`li BM5 őGp7a ֒%Y[UG9@\bDY{{ED0 $Q+FvC`ݨ3Q E\uC9![$l 6DoDgG*+X!%#Cq ?8ZUB)U@opgީZq89|uccAќW;@">Ph_9}.6V/O:3}ZS {:~ykcO6;OB=bV. Rk o ^GV= }oI"+ ]wFzϷ`<30h3]Rf859s`KM8 XUq<\ZOssM&j&  .%PBL~^Gˈ3pD:Z<\ǠiW̆"(:zX~0PG]8RQMNTqfW~!0R%Ց0xvGFy/F-wu/*+ \8@6c<L;c[º nr QS'oQuT{qҐ_ͿSdA*ð:m8Yuz2PB Hh`lkpLLh cEb6eۏҋ ?!>| *=VK@rx0G`%ryr[6Y37 f**n%9df11ޢځ^'] Rq.,^%l e#wWs56!=!q[ %Ԯ]5^:m5)?V b|u7fw,:Ye R% [ o gFAzFPx{dxíw8ٔ{{L> d2CLL,L,(mS$=|%֝lu& ą83 N Xx \VnJ[)Iw/鹻 |GźYDH*Sp60cJ2@W%Ѧc_^$#*:G6n>D;~`9hXB UJB_вˈ%w'$v|#T<68KMϑ-5U+'B ĪNbJOv'|+*Mk(d }C˱@q&aR%} !VЃs3w2a2awHz/Q0F ]~;ä NDP mK3xke_ S!V&=v_PL9؃Yi NU_)J69f*S  17F|BR$y,Ʊ.&=uqsODBR=ɳeؽɇBH 2lu'h7^#S)Xi2..Pe/@FK$](%|2Y1pC8tI11N//+\pjdWmI=߽YZxMЉP81/ JG^U ,Pd1O^ypql2h$jvI%]V .'[+WU8[D,߻-=[ O wE)3J&dقݶR¡S\. 5J$I&oHȳ~ lz> Ux/Hu;?Gt{?;TH L|F8}{p:2t͆aѧp65Y"LD.rVS_ k]n&Hz~9æ p $4ق'{&M\ΰч!qi (.h' B T|{I6cL.빍iI꫿\!;g`1 j%C o3*60E؎]t.-%0 YK_nft] *VFCtJT+\WZ8gF^ ޞf 5I=#6.@2z;W`B/ęQghjyJNAX3, K66ڲM0T@O{4kj|"ftџۄU<-a5b)^R8:ilKa6@!]buvΏ$ oUœ~:.Lte JξP l$S[z~Rq39钺9Q/m"%ʤ7 5MKL鑧"IߏG XTގXLFݧV jp^/Mgۻ{w *9Oʈ<"aAq.M2@mp^'wߕmkxO8 $[&|YZy`2_|%r/J?QṈl3ÞKE$wvCh a@U1M%0?1* $GZ{!|ʿ$ە-٪Ev;͓:`Bl˸쌧ɬoQ0&,F?^s,ch˕$Ecl0w`⏺ň@/r^l8cT3k@JݔuP&ʪNdJjTKi *uX{tj~ɡ}i\BKenȵ|N u#]@lCZ$iPa㸩t04y20 s֪,Au!QBϖ^@Vsɑ\Za7쾉ш6-TrU u~1HJ(<αbRԖqi J?eG *jVħ ":Y);-Fd!HG~ux cb6m)&;0dU?8X~12ۼtIx5{(z '[ŃkZЅi,b1̇`(mHNeK/ [(#QGduT^m%!(7KgP=hϕkɐU+.[eC"GDΨ<*Ř 9&܂?)\<&Ŏ5 LJu@Y,냲ھ_w0^17p޻*>D8_)$UźR!jOF>{ t,-bP,m`D"/zA ͔إQZG&U]xejxLwv~=)@B6?!;53/ps@tOZS7ؙnlxZ?Zj a{6L41 2Qi&֥l]o=7ļ ofЖr MEV@H/aD٦HlK5)ŒZ OE3IG'г;D'zl(E$.ٜ-W R'\w+)w3꺾 @%R).~9;].šg+)%ȝk҉^NW>b1z:soD K2w[|>9vWMFu`axchիU`*ʆe]OV'6xd?H]_rA+zdFH ʋ<ǴkUsFzaH9-gvb=L/E).x9j%B)$AB t b.bAEZRbH(Jya9Wj0fF'Xz $DQ6q` o i={#4FYH@J3 3i~tYТhkHP17YD"pĦ;'16fpu>FoDQin̒- @P# hj ނŀfC 7°T5HVXpklĭ]yXr)?ͺBNJ B#9e&&_0=pZ6h) ̗a b=(p);.N,W^ *hԺCm}E7i6aIvͲxp*Ac#4N&`)ĉHWey7jloEh_n3 jp?4p2WE'kT_ &!ȖjVlHӻ_kɚʳaY s@[G"bYLܫXi Cq8&zVaY{#I@2m!d[1 AƢnKeם/>dmuX:xʷ\pNl+H+ctSǶC[~3e}6 \,Ʉ|Yݧv]'|&M2 ddsx-((76aXm=ӊQ<$Q†\ qiH阇i'i$"{S*VwF/tfQCWUZ{S;Nx}H&* 9׸qU1 a`(M-aG}n̽0 pmcn ɘ_\l} 9FvHþkJZNO mZQҤ aSf )QC+2 d[ H"t* c*bڢq,#S#u'Ҭ:4asCDMF|ɸm_1L]Y\*X>tgDd@&[)8;<{8<+VG\H^aae-4sJA \ hM[\`#pD5Z97g;BWmqTXX%0v&]E 4]FIJ&S_4R0D+meY gO+M{03v'ͅft:;ر Nn\ǔ^,)1laBZZ[  ZSUYh߆wS\/*?zQЋ`X4gr[CWG.Y0Q|RԃE[wy),ш$NK@c/b -#ZI G$ƗtmH#)XwPZAD|S ofTH)>M1b 7ɆSuq jK4[s xL Ǣ]5 !M!AdƧN><:ǻZ(8)e /W| bDDŃtT7rur0Ң`ܴh5 5S}4hrvalc!ZjB]xDbTxzYS6_)op>#@PS*bS\q ƋxYfQ><" Y6IEr_7Ұ VH!IrEL6!Nq"'daqMvA% v n.;A/2ʲa8D$GWv#̏ 9k'o؟o@ (]gk+}/ (nqK(f Ɵиp23YwpDdGq2$}KӯA"E&Ntg'Nes!Ю4qo}쿝S,ojr/s TMT&Qf\12h'&ctN'Tx7]2 ;G ʅ|T++:%/ 1T  ˀ<4͔˗ ,0~!WO' :suҦن(^ﮎ )7fmlҹ1ūtZh L0 6X"J҂ 49 ֩B}ԭ``Ӓ #Jn_F H|$OK=œi17o-Hqp[ɫ%%:Ɉi3۠G CLL4S:dBj|pYSDP>pv5KLe{t0yEND$*;z5NBIgn.N|׶nRaSZJcH mXek;_ 6,yb0#ZA e|wG U1lLD7ÄVqt[xuEQULPBlZSh.1Q0Uٱ8Rip;{H#GON!?t>Q |pkq!gT,j2sǍ4툊tjnƛ/IOE!ˋnF4M&1x$ew+vS  bm]e%8 P !s_06)Q2JB [t9'Ԝ,[fÆג]BB@r&Bs|Q gOC1J D&LJiC`A^#X8tH?daĖTSTaH0@U)^e}Jb7%ܔ%:ƿ@M+ysqL Y00ÔGD >ĩAW 2I:F 32ʠq:6S]K"g[ ϑHB5VEqLJX{CB!PIq9Llxʪ7>֤]@!@9H!pə$ ?)܎l/"́+@`}}:\ 8zQgS+򒤿C}R:HUF\Xg/AZ%c1wlETwX ZNhyf2D ø&vLq47z\iJyJ-kN3 -sJ5)V0N0d\ӛd0d-E[mf\UmxCR<(`ѕp4^!hQ `!l ~ƙ:JɠlW9˸ZXB=l)`jeVJUG!s1?Ƽ3Ê.}bIa6ʕ t?SxZJ'p i,.R2T`5-R BxrWH JPe#Bb|-[PEh‹(5Sfr/]IƊ dE#OS39ӻ]eۮɹ.9_beM9b#e(- 0Ra9"U,%~X܀z۽{'6[@t[W%* .d'vR {h!AedCE}x=E[|B$7J* B- ,=k7[_-I J5e̶{ ( ;WMw`~pAz 8f))(@ Īم<.a%N n@bz>%T*?lgbd<ĵw9Na8;<^*%y:tDҕZ<@0q4l\ 1`/$IJ ғsN);:A;)$ו Wwy%KrIv\bV\nd{6tv/~*O 7U>8rAC<jE-j牷xs)D1Ì/qp**̸$ّ,  Bȼpk MhpK7U]h&-$鎻Y;q6wzW˄֭AhD^R"s5fw +Q&/9ȂwNbz{Y> ]NEc,ߞ# BF:0/-EȾŒ׃F\I{tAZCORuk i)ytkdN&vA P{P'>xƆ`.%,;:Կ:aFoTQ}v#ףQk's~z5hMQʒY>CʍiUNF#J0uC8k! fv {E/IKIE> pyde ʾ=z:@7J|5g8x 3O 3H1؄F.yfzWIM j[.w%i?҆Uf|}@+[8k7CxSEOޯp$Q+:<]K3T-y[Nz;y-HZY^.M*'h8A.N2rLB 7:Or}CS˚S9Jq#WI}*8D!# g#Y>8` В ?a2H,^'?^nhOƒi<Ya2+6aFaMG-Gkè1TbL `*ـVX *xe§֊Z*c`VSbJU*6TK@zqPhg*ߔU(QU49L cM*TR!R,BȅE*C|TzpF@4*텰جXbL.T2y`Upb T,%@` #?@tGLŞS)ÿztϲFy׎ 14Lhfe(.)pK@\ Xe@TbvhD&0-IbD d@ZD1@ DyѧCN| 94Ӛ#Nc l;, `cX@(2$0 "@- $B@<$А8p7C b(@ PA@F 0tGORIJITySMW52\ToRKV0Ȏ( -$ !6wHGO r~e~/]V~/P~7SzKFv`;`9v# JBN,ӭ'`'`\LTApBs)r! ( i`mpire-2.10.2/mpire/dashboard/static/fonts/glyphicons-halflings-regular.svg000066400000000000000000003243021461637447300267220ustar00rootroot00000000000000 mpire-2.10.2/mpire/dashboard/static/fonts/glyphicons-halflings-regular.ttf000066400000000000000000001305341461637447300267220ustar00rootroot00000000000000pFFTMm*GDEFD OS/2gk8`cmapڭrcvt ( gaspglyf}]oheadM/6hhea D$hmtx `tlocao0maxpj name,post5 webfTPT=vuvs Z 2UKWN@ { , h, h@( + / _ "#%&&' ' )9IY`iy )9FIYiy !'9IY` * / _ "#%&&' ' 0@P`bp 0@HP`p !#0@P`fbߵiY!     |vpjdc]WQKED 5 *+  / / _ _  ""##%%&&&&' ' '' !& )009:@IDPYN``XbiYpyaku } )09@FHIPY`ipy !!#'09@IPY `` ((h ./<2<2/<2<23!%3#(@ (ddLL[27>32+&/#"&/.=/&6?#"&'&546?>;'.?654676X& j  j )"& j  j )L j )"& j  j )"& j LL#32!2#!+"&5!"&=463!46^^L^^p@LE32!2+!2++"&=!"&?>;5!"&?>;&'&6;22?69  x } x }  x } x v L   d    d  l d;2#4.#"!!!!32>53#"'.'#7367#73>76p<#4@9+820{dd 09B49@4#bkv$B dpd>uhi-K0! .O2d22dJtB+"0J+ku0wd/5dW%{L>G!2+!2++"&=!"&?>;5!"&?>;4632654&#^CjB0  0BjC x  x u x u@--@$?2O*$ $*P2@%d    d   BVT@L!2#!"&=46 %A+32!546;5467.=#"&=!54&'.467>=2cQQc22cQQc2A7 7AA7 7Ad[##[[##[dd76!' Pԇ $ op zy#%**%$ pdL #7!2"'&6&546 6'&4#!"&7622?62~   \l lL 7  &   l 2'7' & c_"fn &\`tfjpO32!546;! 22&&L%6.676.67646p'0SFO$WOHBXAO$WOHB"7Q)mr *`)nq&* )2"'#'".4>"2>4&ȶNN;)wdNNrVVVVNdy%:MNȶ[VVVdXD>.54>0{xuX6Cy>>xC8ZvxyDH-Sv@9yUUy9@vS-H^{62!2'%&7%&63 a o  ^{"62!2'%&7%&63#7'7#'JJN a o  d⋌&2##!"&=467%>="&=46X|>& f   f &>|.hK  ]  ]  Kh.| L#'+/37GKOSW!2#!"&54635)"3!2654&33535!3535!35!"3!2654&35!3535!35~  Ud  & sdd dd d  & d dd dL   ddd  ^ ddddddddddd  ^ dddddddddLL/?!2#!"&546)2#!"&546!2#!"&546)2#!"&5462pmppmpLpppp LL/?O_o32+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=462LppL/?O_32+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=462DDDLpp&,  62"'&4?622;;nnBB# "' "/&47 &4?62 62    ;    %I2"'#".4>"2>4&3232++"&=#"&=46;546ijMN,mwbMMoXXXX K  K K  KMbyl+MMijMXXX# K K  K K %52"'#".4>"2>4&!2#!"&=46ijMN,mwbMMoXXXXX^  Mbyl+MMijMXXX  -32+"&5465".5472>54&&dd[֛[ҧg|rr|p>ٸu֛[[u'>7xtrrtxd/?32+"&54632+"&54632+"&54632+"&=46  ޖ  ޖ  ޖ    ~ p     >     GO27'#"/&/&'7'&/&54?6?'6776?6"264X!)&1-=+PP08,2&+!)&1-<,P  P/:-1&+x~~~P09,1&+"(&1,=,QQ09-0&* !(&0-=,P~~~d!%)-1!2!2!5463!546!5#!"&53333333,);  ;),,;)D);dddddddd;)d KK d);ddd);;) dDDDD 62++"&5!+"&5#"&l`    j`  w  ? d3!#!"&5463#"&=X;),Rp);vLp02".4>"2>4&3232+"&546֛[[֛[[rrrr|2   [֛[[֛;rrr   2  ^  )#!3333))p,p,d/3232"'&6;4632#!"&546;2!546& & T2   2 >p  ^  12".4>"2>4&3232"'&6;46֛[[֛[[rrrr|  & [֛[[֛;rrr   12".4>"2>4&%++"&5#"&762֛[[֛[[rrrr   &[֛[[֛;rrr  9!2#!"&'&547>!";2;26?>;26'.    W & & W tW    >     '2".4>"2>4&&546֛[[֛[[rrrr[֛[[֛;rrr] $  (76#!"&?&#"2>53".4>32  mtrrr[֛[[u$  Lrrrtu֛[[֛[576#!"&?&#"#4>323#"'&5463!232>  ntr[u[u  h ntr$  Krtu֛[u֛[v h  Lr d/?O_o!2#!"&546!"3!2654&32+"&=463!2#!"&=4632+"&=463!2#!"&=4632+"&=463!2#!"&=4632+"&=463!2#!"&=46}    R 2  2   > 2  2   > 2  2   > 2  2   >   ~   R d 2  2  2  2  2  2  2  2  2  2  2  2  2  2  2  2 L#54&#!"#"3!2654&#!546;2uSRvd);;));;) SuvR;));;)X);dLL 732#462#".'.#"#"'&5>763276}2 d!C@1?*'),GUKx;(.9)-EgPL 3 0[;P$ 97W W!1A2+"&54. +"&54>32+"&546!32+"&546ޣc 2  2 c*  `  ct  ,rr  ,tޣ 4  4  G9%6'%&+"&546;2762"/"/&4?'&4?62A   Xx"xx"xx"ww".   ^ x"xx"ww"xx"r/%6'%&+"&546;2%3"/.7654'&6?6A    `Z  HN.   ^ d  g~jb1K3#"/.7654&'&6?6%6'%&+"&546;2%3"/.7654'&6?6 D@  *o;7 *    `Z  HN iT "ZG !   ^ d  g~j  !%-;?CGKO3#!#!#3!##5!!!!#53#533!3533##5#535#5!!#53#53#53!5!ddpddX,,ddddD dddd,D,ddddd dd,dddX d,,d,,ddd dddddd,dddddd  #7#3#3#3#3#3!5!#53#53#53ddddddd,,dddd,Pdd[[[[[   "'463&"260V C;S;;S;V0 ;;T;;  ! "'463!"/ &"260V 08D;S;;S;V0 V08;;T;;d&!2&54&#!"3!2#!"&54?6,9K@  D@   K|@  @  J  L !2 46 >>CEU!"3!26?6'.#"#!"&/.+";26=463!2;2654&!"3!26/.6D N9  >SV N N      & X & l l- p  v       dL!)13232#!"&546;>35"264$2"&48]4$);;));;) '3]dϾV<?!(% _5,Ry:" *28 T2*BBW-ޑY". BB % Zd'2;#!5>54.'52%32654.+32654&+50;*7Xml0 ); !9uc>--Ni*S>vPR}^3:R.CuN7Y3(;  G)IsC3[:+ 1aJ);4ePZo!56764.'&'5mSB ,J   95(1(aaR@ 9%/#4.+!52>5#"#!#3'3#72 &2p"& 2KK}}KK} dd R ,১ !%/#4.+!52>5#"#!5!'7!5L2 &2p"& 2C১  vdd  ,}KK}}KKL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=462X LLddddddddL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=46DLDLLddddddddL/?5463!2#!"&5463!2#!"&5463!2#!"&5463!2#!"&Xp LddddddddL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=462LLLLLddddddddL/?O_o32+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=462ddA ddA ddA ddA LddddddddddddddddL#*:J!#;2+"&=46!2#!"&=465#535!2#!"&=46!2#!"&=46dddd ,XLdddd}KdKddddL#*:J32+"&=46#3!2#!"&=463#'7!2#!"&=46!2#!"&=462ddgdd /ȧ,XLddLdddK}}dddd!2#!"&546 K,,,,,,v,,,D,,L!2#!"&5467'2"&4,XJ*J%pNNpNL d>tNoOOo62.'&54>"264usFE66 !^Xm)!fhHuXyHÂ2".4>"֛[[֛[[Ktrr[֛[[֛oVrru5.54>6?6&'.'&76#&*IOWN>%3Vp}?T|J$?LWPI)(!1 )  HuwsuEG^F&:cYEvsxv!K:%A'# " A)Y l */7>%!2!"3!26=7#!"&546 7l l27);;));Ȼp87cs* s ;) );;)2cL6!#"3!2657#!"&546&'5&>75>^i4);;));ȹpS 9dTX .9I@F* L6;) );;)g  0!;bA4 L5!2!"3!26=7#!"&546 62"/&4?622^^  Ȫ   ȯ  ȭ   ȭ   L326'+"&546d0dLJJL#3266''+"&5462d00dLJJJJ3''&47660J*J36 &546.2   d32+"&546!32+"&546  dL#!"&5463!2L  346&5&5460d * ;O#72#"&5&5&5464646dd12N: 9  > =,L32+"&5&54646Rdd0L;;dH  #!"&762!2#!"&=46  *9HdduJ  u`((&;(J ' 7(a#aa32".4>#"#";;26=326=4&+54&֛[[֛[[}dd[֛[[֛dd2".4>!"3!26=4&֛[[֛[[E [֛[[֛~dd32".4>"'&"2?2?64/764/֛[[֛[[ xx  xx  xx  xx [֛[[֛ xx  xx  xx  xx  $2".4>'&"2764/&"֛[[֛[[Tw[֛[[֛1Uw;K2".4>";7>32";2>54.#";26=4&֛[[֛[[?2".4>#";26=4&#";#"3!26=4&+4&֛[[֛[[    KK  ^  K[֛[[֛V   2  2  2  /_3232++"&=.'#"&=46;>7546+"&=32+546;2>7#"&=46;. g  g g  g Df  fD Df  f g g  g g ͨ  fD Df  fD Df?2".4>"2>4&"/"/&4?'&4?62762֛[[֛[[rrrr@||@||@||@||[֛[[֛;rrrZ@||@||@||@||02".4>"2>4&"/&4?62762֛[[֛[[rrrrjjO[֛[[֛;rrr}jjO!2".4>"&32>54֛[[֛[[KtrAKihstr[֛[[֛;rtxiKA>rtsS6!2#!'&4' &F   &S &5!"&=463!46 &U & U ## ] #!+"&5!"&762   && ]32!2"'&63!46&# U & U # &] &5>746 ^$,[~UU & U #$DuMiqF +!2/"/&4?'&6!"&546762R,^j^!^j^^j^P,^j^IIgg+#!"&546762!2/"/&4?'&6j^^ ,^j^`j^,^^j^/2".4>#";2676&#";26=4&֛[[֛[[:#6#:1  [֛[[֛.   IUaho276?67632;2+"!#!54&+"&=46;2654?67>;26/.'&;26!"&5)#!  &0  =  2 pp 2  =   353  X  v  v !{,  2  ,ԯ  2 0y    r w  +I6.'&&&547>7>'.>7>&67>7>7>-BlabD8=3*U  :1'Ra\{%&=>8\tYR-!q[Fak[)ȕX1 "@&J<7_?3J5%#/D &/q!!6ROg58<'([@1%@_U2]rO.>7'&767>.'&'.'&>77>.'&>' '8GB    `H  >JS>H7 '+" NA 5M[`/Pg!;('2"&"IbYCe\D9$ 886#1%)*J7gG:    8G\au9hoK$]54<&"&5476&2>76&'&6?6&'&'.{nO9:On{{nO:9On{FZ  2Z__Z2  Z# %8-#,- "F-I\b\I*I\b\I--I\b\I*I\b\I9>||;7Es1$F^D10E^E$1u$/D0 "%,I';L!#7.54>327377>76&'&%7.5476&6?'&'.P[vY,9On{R=A &/l'PjR.Mv&  6QFZ  *HLh5)k|# %8- ,- "xatzbI\b\I-yRU4Zrnc1?1FrEs11) ]@ @] )1ES>L'+/37;?CGKOSW[_c3232!546;546;2!546#!"&5353353353353353533533533533535335335335335Rd22ddddddddddd|ddddddddd|ddddddddd2222pddddddddddddddddddddddddddddddw%7&=#!"&=46;3546'#"&=463!&=#'73546oXz#z*dXzdM*zL!2#!#"&546d);;)d);;L;));,;)X);dL ?32!546!32!546".5!2>&54=(LffL(, '6B6'p)IjV\>((>\VjI), +'%! !%'*L 'L'a'M 7 Maa'aQd_)!232"/&6;!%+!!"&5#"&?62**p&032!2#!!2+"&=!"&=#"&/#"&468^&d,!02**6%%+*2222 *L !53463!2!!P;),);DPdd);;)L 3463!2!!;),*:,P, pX);;)dDEk+32"/&6;#"&?62{**YDk&=!/&4?6!546X`)  )   !.#!"!"3!26=4&53353$`$-);;));;ddd-(d;)d);;)d);dddddL #12"&54%##"+"&'=454>;%".=4>7i**d]&/T7 " LRQ  )2( Jf,53232#"./.46;7>7'&6327"&)^Sz?vdjO9t\U>/ v?zS$2451 7F8%M)(  ()GM~ 1==7'''7'7'7'77 N괴N--N괴N-N--N괴N--N괴d!-=32!2+"&/#"&54?>335!7532+"&5462(<H(<,F=-7` 1dd>2vddQ,}Q,d-!2$'$(ddw} L 0<32#!+"&/&546;632+"&546!#35'!5X,<(<(21 `7-=|dd_dd22L!-d,Qv,Q($'$dd dԯ}wdO7G%6!2+#!"&5467!>;26&#!*.'&?'32+"&546dkn  T.TlnTj:d%8   VOddip &yLN(  % H YS(22S dO6F#!"&'#"&463!'&6?6*#!32!7%32+"&546n jUmlT.U  nJ   %&jPddO (SNLy& pd(Y aL7G2#!"&/&?>454&/!7%.!2#!"&=46ސNS( % p &y22SY( nTjkn  T.T8   Vd% dd-I!26=4&#!""&5&/&7>3!2766=467%'^ NLy& p  (S22(SYLddjTnlT.T  nk V   8%d%2".4>%&!"3!7%64֛[[֛[[  [֛[[֛9   &%2".4> 6=!26=4&#!54&֛[[֛[[%  [֛[[֛ &   %2".4>&";;265326֛[[֛[[K &   [֛[[֛@  %2".4>#"#"276&+4&֛[[֛[[  & [֛[[֛  2".4>%&277>7.'.'"'&65.'6.'&767>'&>7>7&72267.'4>&'?6.'.'>72>՛\\՛\\d+: =?1 " "/ ?9 #hu!$ 0 E.(,3)  (     *!A 7 ,8 !?*  \՛\\՛  ' "r"v G  .&* r$>   #1    %  *  '"  $  g2( % 67'"/&47&6PM<;+oX"O\e~Y+" n+We`#'7;!2#!"&=46#3!2#!"&=46!!!2#!"&=46!!d);;));;);;));; );;));;,;)d);;)d);dd;)d);;)d);dd;)d);;)d);dddL !2#!"&46!|;**Dd%32!2!5#!463!54635#!"&=);,); ;),;);));;)d;)pdd);d);dddD);;)+AW!2"/&546)2/"/&4?'&6#!"&54676276#!"&?'&4?622,^j^5,^j^/j^^^^j^j^,^j^&j^,^^^j#;CK2".4>"2>4&$2"&4$2#"'"&546?&542"&4$2"&4ݟ__ݠ^^oooo-- - L- 73H3)z - - - - _ݠ^^ݟWooo -!!- -! $33$ 1~ - - - -Z[%676&'&#"3276'.#"&477>32#"&'&6767632'."[v_"A0?! -  Y7J3$$ )G"#A.,= # (wnkV8@Fv"0DG([kPHNg8B*[eb2!5(7>B3$$' )M"#!7)/c# *xnfL@9NDH7!$W]B$&dXDD>.54>"".#"2>767>54&0{xuX6Cy>>xC8Zvxy#!?2-*!')-?"CoA23:+1! "3)@ +)?jDH-Sv@9yUUy9@vS-H-&65&&56&oM8J41<*.0(@  )*D*2Om9w.2&/7'/&477"/&4?BB8"._{iBBi BBBBBB7._BB^*k"5._{jBBFi BBBBBB77/_2#!"&54>!"264d:;));XV==V=.2G);;)3-D=V==V "/''!'&462*$3, #**#4$*' 2@K#.'#5&'.'3'.54>75>4.&ER<, 3'@" MOW(kVMbO/9X6FpH*M6&+  4C4%dfJ2#4.#"3#>36327#".'>7>'#53&'.>761T^'<;%T)-6"b "S5268 jt&'V7  0 $ݦ -$aPN(?",9J0* d2>2 ""   7Gd/9+DAL!X32"/&6;3+##"&?62*Ȗ*,|%#5##!32"/&6;3353!57#5!ddd,*dc,dd|ddd!%32"/&6;33!57#5!#5##!35*X,ddd,d,ddPdddL32"/&6;3##53#5#!35*Xdddd,d, dPddL32"/&6;3#5#!35##53*d,ddd, ddd32"/&6;3#53!5!!5!!5!*d,dpd , 32"/&6;3!5!!5!!5!#53* dpd,d, LL!2#!"&546!"3!2654&^pg );;));;Lp;) );;));LL+!2#!"&546!"3!2654&&546^pd );;));;oLp;) );;)); $  LL+!2#!"&546!"3!2654&!2"/&6^pg );;));; $ Lp;) );;));LL+!2#!"&546!"3!2654&#!"&?62^pg );;));; p $Lp;) );;));L5!2#!"&=463!2654&#!"&=46&=#"&=46;546&p);;)>DLpd;));d&  #%2"+'&7>?!"'&766763 ,  P'' K    S#  nnV/L5!2#!"3!2#!"&546&=#"&=46;546^>);;)pDLd;) );d&  1!2/"/&47'&6#"3!26=7#!"&5463!m)8m);;));Ȼp,pm)8m;) );;)֥#2".4>"2>4&2"&4ٝ]]ٝ]]qqqq{rrr]ٝ]]ٝGqqqsrrrL#3232"'&6;46!2!54635 ' gdV^|d22L# ++"&=#"&7>!2!54635Gz " 'gdM !d22LK" 62"'&4?62!2!54635qgdq#d22L #'762'&476#"&?'7!2!54635*MMК=gdML*Л:d22L#'/'7'&6"/&4?!2!54635^WЛԛL*MgdКԚPM*MXd22% ! q3gqdL+!#"&546;!3#53LDdddp,E/'&"!#"&546;!3#53"/&4?6262L_  Ȗdddj\jO)_ p,j[jO) >'.!#"&546;!3#53"/"/&4?'&4?62762Lg%dddFF))FF))gp,F))FF))F/!"!#"&546;!3#533232"/&6;546L dddd*p,/'&"!#"&546;!3#53++"&=#"&?62L*ndddd*pp,L !2!546#!"&5!52LPdL&}-1;&=!5!546#"&=46;#5376!!/&4#5;2+p/22ddpddd33*ȖdȖ*yddQ%6+"&5.546%2+"&5.54>323<>3234>^%"% "  d d 1t5gD >?1) A..@  ^  ^ dL3"!5265!3!52>54&/5!"!4"2pK Kp"2KKL8 88 %v% 88 x88 %v% 8LL  $(4!2#5'!7!!2#!"&546!55%!5#!!'!73wipdw%,);;));;),p,ddibbd;) );;));dfdd&767>".'.7.wfw3 .1LOefx;JwF2 1vev/ 5Cc;J|sU@L#A2/.=& &=>2#!"&=46754>ud?,  1;ftpR&mm&L!((" """" '$+  222/2 ! '!'3353353!2+!7#"&46!2!546L J LP*dd*22dL #"!4&#"!4&!46;2d);,;gd);,;;)d);L;));;)D););;)L%)!2#!"&546!#3!535#!#33||D| ,dddL| |||Dddd,ddd,L%)!2#!"&546!#5##3353#33||D| dddddddddL| |||Dddd,L#!2#!"&546!#3!!#3!!||D| ,,L| |||DdddL!2#!"&546!- ||D| ,L| |||D ,L )!2#!"&546!!!#";32654&#||D|dDd&96) )69&L| |||DdVAAT,TAAVL%)!2#!"&546!#3!535#!##53#53||D| ,ddddL| |||Dddd, d dL#'!2#!"&546!3!3##5335#53||D|DdXddd,ddL| |||Dp ddL"&!2#!"&546!#575#5!##53#53||D| d,ddddL| |||Dp2Ȗd d d %2".4>"2>4&!!!'57!۞^^۞^^qqqql,dd,^۞^^۞Lqqqddd '+2".4>"2>4&#'##!35۞^^۞^^qqqql2dddd,^۞^^۞Lqqqd2d2dddddA 62632+54&#!"#"&5467&54>3232"/&6;46n,,.xxPpVAbz  & AwasOEkdb  A32632&"#"&5467&54>++"&5#"&76762n,+.yxZ % OqVAb   AwaxchsOEkdc  dLm%5!33 33!#"!54&#Ԫ2dd,,Md22y7/2#"'2!54635#"&547.546324&546X^Y{;2 iJ7--7Ji/9iJqYZ=gJi22iJX5Jit'*BJb{"&'&7>2"3276767>/&'&"327>7>/&'&&"267"327>76&/&"327>76&/&oOOoSSoOOoS=y" $GF`   Pu "Q9   ccccVQ:   Pu "GF`   y" $ooSWWSo++oSWW"y  `FG # uP  :Q # cccc:Q # uP  $`FG # "y  d "!#5!!463!#53'353!"&5+, ?,dԢdu       d !! 463!#5##5#7!"&=)+5, ?,>dԪ |  ^G |d 77 P#3!#732!!34>3!!ddԢ!,d!s, d,+$d$+ppLL293232#!"&=46;54652#!"'74633!265#535d22s);;);)X>,>XL2dd2;));FD);>XXԢddL6=3232#!"&=46;54652#3#!"&54633!265#535d22s);!);;)X>,>XL2dd2;) $+;) );>XXԢd  #!"&762#";2676&35} ,, }@D:#6#:&77&P'L. dd LL/?O_o32+"&=4632+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=46                  L                  )33#!2!&/&63!5#5353!2+!7#"&46!2!546dd^>1B)(()B1>^dd> J LPdO7S33S7Odd|*dd*22+52#4!!2!'&63!&54!2+!%5#"&46!2!5460P9<:H)"Z" )HJLP;))%&!!&**22$.2"&432!65463!2+!7#"&46!2!546 jjj."+''+# J LPjjj9:LkkL:9r*dd*22,62"&5477'632!65463!2+!7#"&46!2!546X/[3oo"o"."+''+# J LPk6NooN>Qo 9:LkkL:9r*dd*22",!!.54>7!2+!7#"&46!2!546X,%??M<=BmJ J LP9fQ?HSTTvK~*dd*22)2!546754!2#3#3#3#!"&546/R;.6p6.d6\uSpSuu;)N\6226\N)G6.dddddSuuSSudLL/3!2#!"&546!2#!"/!"&4?!"&=46!'|  % XW & dDdL D 2  % XX %  2 dddL#-7!2#4&+"#4&+"#546!2!46+"&=!+"&= Sud;));d;));du);P;ddLuS);;));;)Su ;),); 2222  !&4762 !2!546 'YV/ |UYY(n0U22!/.#!"3!26=326!546;546;33232!'p'q*}20/222,2 "!#!5463!#5!#!"&5463!#5,  w,, v  w, O,T    dGFV32676'&7>++"&?+"'+"&?&/.=46;67'&6;6#";26=4&KjI C   )V=>8'"d 1*) "dT,| -otE  GAkI ! "% ,=?W7|&F@Je5&2WO_e_ 2  2 ~ $4<Rb%6%32!2&'&#!"&=46#";2654&'&"2647>?&/&6%?6'.'.. +jCHf7" *:>XXP* @--@- -?0 !3P/|)( )f!% =  &* x"62&CX>>X83 D-@--@ۂ # =I+E( //}X&+ 5!H d9Q`o322#+"&=#+"&=#"&=46;#"&=46;546;23546!2>574.#!2>574.#q Oh ..40:*"6-@# d   KK   d)  )k)  ) m!mJ.M-(2N-;]<* K  KK  K X K  KK  "p "),!2#!"&'.546"!7.# Vz$RR(z }VG+0 )IU!zV`3BBWwvXZ3Vz&--% ,(1#32#!"&546+"&=ۖgT)>)TH66g )TT)g6633#!"&546+"&=`T)>)TH66B)TT)g66 %'5754&>?' %5%Ndd/\^^<ǔȖ  (Abd 2"&4$2"&4$2"&4|XX|X|XX|X|XX|X X|XX|XX|XX|XX|XX|L2"&42"&42"&4|XX|XX|XX|XX|XX|XLX|XX|X|XX|X|XX|ddLL/!2#!"&=46!2#!"&=46!2#!"&=46}  J    J    J L  p  p  /3!2#!"&546!"3!2654&!2#!"&546!5^ );;)X);; G ;));;)X);d,dddL;!2+32+32+32#!"&46;5#"&46;5#"&46;5#"&46222222222222L********, *.62"&%#462"&%#46"&=32W??WW??||||||*(CBB||||԰||||ӐB76+2+"47&"+".543#"&'&676/!'.6E*  '?) T 0I' *L #3{,# n  6F82 *5#"#!#4.+3#525#"#5!2 &2p"& 2D d 2d  dd R , W 22 L 05"'./#!5"&?!##!"&=463!2E  1;E%= !'y,2 " # 22+."A2VddGJ!2#!"&546#"3!26=4&#"'&?!#"3!26=4&'"'&'#&#2LFF &  7 ? 9   9 gLR   2 2  2 2 $ #'!5!!2#!"&546)2#!"&546!PpmpG,Ld|pd,#'!2#!"&546!2#!"&546!!5!2pmpG,P| pd,dd'+!235463!23##!"&=##!"&546!2dddpdp,d ,'3#3!2#!"&546!!2#!"&546dddpG,|dpd, pdL'+32+!2#!"&5463!5#"&546;53!X|^d,Lpdpdd,'!#3!2#!"&546!!2#!"&546ddvpG,|dpd, p,0o #"&54632a5*A2~ 6'&4O**{))*2A~ !2"'&6d)***2,~o #!"&762{))*a**( 5-5!5!Lc d 1#3!35#5!34>;!5".5323!,P2 &d2"d& 2dd,dd  dd & ,L%1#4.+!52>5#"#!#3!35#5! 2 &d2p"d& 2 ,, dd & ,dd,ddfrJ32 +"'&476 0  ) J 00   >fJ32+"&7 &6S )  0 J ))   fJr"'&=46 4 ))  w  )  0f>J ' &=4762j  00  )  0  =:#463267>"&#""'./.>'&6|Vd&O "(P3G*+*3M, :I G79_7&%*>7F1 ||5KmCKG\JBktl$#?hI7 !2+&5#"&546!5X,p dddL!2%!#4675'=DXDd dQ,[u}4]ddMo__<vsvsQQ(dpEHEd{ d&ndd ddddd5d!u ,d;I]ddQEJadd9'dddd dy'dddddddd,d,A22>ff****NNNNNNNNNNNNNN"~Fn2b\r bb 6 ( L 0  X * ^ h(T*v 8|t*<6`R.j(h6h^2Dl.vb F !2!v!"@""##"#8#z##$$0$^$$%4%`%&&~&'P''(4(p())*&*J*+ +z,,h,,---.(.f..//F/~//0>0011`112$2^223"3>3h344`445,556>6|677N7788B889 9J99::l::;;<:>>?(?n??@H@@AA~BBBCCBCvCCDD`DDEZEFFtFFG6GvGGHH2HNHjHHII8I^IIJJ.JR@. j (|  L 8 x6 6   $ $4 $X | 0 www.glyphicons.comCopyright 2014 by Jan Kovarik. All rights reserved.GLYPHICONS HalflingsRegular1.009;UKWN;GLYPHICONSHalflings-RegularGLYPHICONS Halflings RegularVersion 1.009;PS 001.009;hotconv 1.0.70;makeotf.lib2.5.58329GLYPHICONSHalflings-RegularJan KovarikJan Kovarikwww.glyphicons.comwww.glyphicons.comwww.glyphicons.comWebfont 1.0Wed Oct 29 06:36:07 2014Font Squirrel2       !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~     glyph1glyph2uni00A0uni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni202Funi205FEurouni20BDuni231Buni25FCuni2601uni26FAuni2709uni270FuniE001uniE002uniE003uniE005uniE006uniE007uniE008uniE009uniE010uniE011uniE012uniE013uniE014uniE015uniE016uniE017uniE018uniE019uniE020uniE021uniE022uniE023uniE024uniE025uniE026uniE027uniE028uniE029uniE030uniE031uniE032uniE033uniE034uniE035uniE036uniE037uniE038uniE039uniE040uniE041uniE042uniE043uniE044uniE045uniE046uniE047uniE048uniE049uniE050uniE051uniE052uniE053uniE054uniE055uniE056uniE057uniE058uniE059uniE060uniE062uniE063uniE064uniE065uniE066uniE067uniE068uniE069uniE070uniE071uniE072uniE073uniE074uniE075uniE076uniE077uniE078uniE079uniE080uniE081uniE082uniE083uniE084uniE085uniE086uniE087uniE088uniE089uniE090uniE091uniE092uniE093uniE094uniE095uniE096uniE097uniE101uniE102uniE103uniE104uniE105uniE106uniE107uniE108uniE109uniE110uniE111uniE112uniE113uniE114uniE115uniE116uniE117uniE118uniE119uniE120uniE121uniE122uniE123uniE124uniE125uniE126uniE127uniE128uniE129uniE130uniE131uniE132uniE133uniE134uniE135uniE136uniE137uniE138uniE139uniE140uniE141uniE142uniE143uniE144uniE145uniE146uniE148uniE149uniE150uniE151uniE152uniE153uniE154uniE155uniE156uniE157uniE158uniE159uniE160uniE161uniE162uniE163uniE164uniE165uniE166uniE167uniE168uniE169uniE170uniE171uniE172uniE173uniE174uniE175uniE176uniE177uniE178uniE179uniE180uniE181uniE182uniE183uniE184uniE185uniE186uniE187uniE188uniE189uniE190uniE191uniE192uniE193uniE194uniE195uniE197uniE198uniE199uniE200uniE201uniE202uniE203uniE204uniE205uniE206uniE209uniE210uniE211uniE212uniE213uniE214uniE215uniE216uniE218uniE219uniE221uniE223uniE224uniE225uniE226uniE227uniE230uniE231uniE232uniE233uniE234uniE235uniE236uniE237uniE238uniE239uniE240uniE241uniE242uniE243uniE244uniE245uniE246uniE247uniE248uniE249uniE250uniE251uniE252uniE253uniE254uniE255uniE256uniE257uniE258uniE259uniE260uniF8FFu1F511u1F6AATPmpire-2.10.2/mpire/dashboard/static/fonts/glyphicons-halflings-regular.woff000066400000000000000000000556001461637447300270660ustar00rootroot00000000000000wOFF[\FFTMXm*GDEFt DOS/2E`gkcmaprڭcvt (gaspglyfM}]oheadQ46M/hheaQ$ DhmtxROt `locaS`'0omaxpU jnameU,postWH- Ѻ5webf[xTP=vuvsxc`d``b `b`d`d,`HJxc`fft! B3.a0b ?@u" @aF$% 1 x?hSAiSm߽44,qPK q XE](2 .ԩ] "ED i]DԡZJ\8wwV"FpUԯ.Χ(gK4O n;NR{g`'!PMUHEՠJʫ*Yq9c<U9!QIYׅ-KC+ դU)Q94JYp]Nq9.qyVV n)9[{vVכ־FWb++{>׍a|*gQ,K<'W@Ex̢D&Ud# & x Mx2c 5*.lN/h]GtT(xŽ |յ0>wm#Ye[%Y-YR'rYjD% ,@BKZjHڤ@b-R+nhK~룼$;h^fܹsn{ι ˴0 kb8Fd:%Lה"1AՔ AY>,ؔ#pZ4؟5made ?Ȝy=I:C D(nIxL .1!P'JDtHj@L4Ph' )b)vHX,f1c\'cGu>1 ~t?!xT_q?qBF#L%Dћ"?Yǯj??8>NSkemAYDb4 J);@jP$ 'qh8`;aX6CF*dYc"'?hLV㗌,>ce3eVh =C~xC\((qb@ 4xK&hׁ 4\2DZ6N1|-;j Yu@jѫxi䊧mK ٍDEwq3̷.cAw@4t.gkgr{~Wl~{lW2} 276a2\6oz@$HSH gbtX70Ktc1,7B oLƏ66[,%iZ ,l>TpKSGg\> #A#3Eyk6v;u3!ZI8Mk?8CWq{`C*h>H1_skh)ojOO' !~dXgB(0< kOYxeƧĭ5k =d ϧ> +tC-o Ǫ/_koܶs+fOztpu7-}d9 se \9.H4!0S\ ʱk2"?ip7\2zlްt=W\!KyOXimUnov 6: 2 LZkAA^qCޔ &PaFI0>&Q #FQl> A·q*OȦ_@27l,sf 6p7ܩ?M1vA2]$j";vlk~va0gjzRD:gc6yw%g(þ#'uB#=_@?>FVb0a!aL4tXv:Fh9j^xތz}Wn}7}jΚiHitKSaXEEbbBQ1ftxFȮ -"dqA\~F`6i䁕+ Ԣ^Ȳ}ש׆k&Ĺ<- \;g1>w00v^x 7l#Ot^5+xe.^]׼G8^ m(t1 sbfJ %<4H@e8C,5<(kc5YIA]|ךl6+=HVcbKՋB6i4 #_|&>NvQk#pW=u7HɰR$ [5싙 g %19}&@$&l=1RI}9#ςz??1z&ı_ac|PI[:u;l->k4GYm|Zw }HnR=-B ~m.ِ .Mz^,0%8EG**|sg|ozO֬0sz.WN^ yHk<v3t{8-|' ea~H94xA-@y bT4@0b#]DDljDSio:AgSP z:;-|yH"r {B{\5RLi6AAtM]taRKC!1CgC샂 +1EG!Xzٛnzv@x-#i^x*$)W=O\f[WX~V? `Lei::v4$?=Ra#c]8YFJb&'{%LCE Cf]^$/fߪM;À; 6CXV#X~ F< :vCcyBpLv1Fv#9 /8VF01_K?x>}#G7т\Wp!.@bwɡ+{o#ԍPQҮnī66 cZD(. u;nM}?vtxF{+` ="rPπlDV̶?Z@H䰅][35%O )\^ Z;>Ftf-IzӮ yu1uo<:oa:uqwykk ⋜}0?jvX+}VG$s ?26YI5c$Cfb!X*|F^$p7p55߶6[mjgl>* KO& 8ܝ:ǰokKm~oS-*4E}P/% k:e"1AJCAX8= LŢ>ܱav{|K.3 :\Bxwbeb>1ۿvH?f58 %6$ɲ'pL^HXbpIVqnA8Kg'i!UzSEI5N=hpV?(E Vr?޴7Vڋɿ.O;p 4NRZm.O> MuL'j5`;MtAQܶMyV<` $m)yڳXDa:݁q1JFq15-l\3~X-2pFDe/f!2i:=h{%{t^ *PBͽ]YD3jd *w|GLϽ}ˑk7Ç=06oz*zo1~Jw00SePw%#@BJB %+ ';%!& )Hq 7fqH.!Eǎf,9՚$9 H{~i Z)O|!"D.KQ a2 %2Wɂ\{*B{7,9.'ew U^W&$r9rcGBwll<ʷSQゅh! iѨvJ :Y?#_m4q[ },EA{VПP|Dg?9MId?{)/ /\[ Jҏ[f4G>QK^ m O -7w]„<U3jƏ,:Yq~0/mŵ@CCFq{,Θ쬷ΘQSo lsɿh?A2q`5Z&*X1L5:6ς+O]uej%?ۼ&aW?{2[}W?JbΙk-\b7sIkf&Λfx~nO-9V ~cW"ȗy)b\)2MrWf;MU7'[-c/.ؾuMl&.9) G!!W* 60Cф#qrqOKZOWq,8́/XpTȑg<>¤)[J8o` ;S\S%h~p|J˾F~K=E0NQX*8;D7Q1QC% *Eyy} UG?>I`>'6<+3IVgϮyOQ$WBvH v[Ϗ 2+ 'ø6N߆<ɕ 2S娚9X1\┣df>B~-t>W]pPrZ['+ƌl9]8qC!' @AAOuШ !?M\JMͭfǞ)ߕ=w?AN>¼}jQ<ǏpǠ^(}1+2q F4RiHďITr8^!gm>'ڸhE`s̊ol!(9~ o%#)~ƃj$@ՔLpGOa{߿fé)zؔY<~^cs潺ݴNRURTY%8Ks3qd]^QTb' zx)HFҩPmUZjQ&XƁo<0jYGz]$8c&hyݼwΞ{9^sf߹m[vӣ!(ZAsۧyB8RiԣBg6{UmtyW!bpǮd n/ŷʼ@v/%cxEn:4Y²,yZ-krcH&^ȩC'Ȯ'^T5r)((IJU&#݌! +YM.JEX^|Lw@ھZsgY洺\xԟxyLCyo?eV"_[Q/5Y|qI/\9diEBh$v wOL fpa ,?HgHf2RbL v >USo^1/,ēvcYGmŨ~Amz ?/40yj̸pk2H eERb/"M 75ul[drC&Y͐&I `!>p;J-b--.VM4>Fj/5σt5}>C*<'d?,cdGf2ҁ0w6Lh"fKζp;ǿ϶Pdc1EOi%Ř(DCWV2I)TiMFTz0U S7V mBW6;nYZUzSTg>(hF"޽T뽷R]L۶|Lx[s,'NU|E<4)Rp*vU#g*gjə*=~܃ASēA JHw3@NurbwȀʌx}[`7ZtPlh L.)NU}kq'vFQr׷{ˤS]ZL(@*Sf^+uPe_k#.8ɂ%ՠ,@TKх t`ߑXAD;b|pA7}q2 @Y`~iԬK0jY( R~^ҧ8>=F"˜A[DqvQCX|ZsO \/f.F;kPbdz7ԐeͶ-6bybaWjnh7YLF!4wssFCnh_0> MZ nC *#5/OUN\(3o@[7`Mg8xge;f\y|f֤ޑ]i5q5q&>'353kYꭑ=W7+΋yxIeOYǏs(p6[B/t爁*̠-n: <Ц) +ް~q_}oxt>LV FG@d9[2?2ȳ8笞={fgcsCmre#E>45qo:JX^ioP,xf:/yn9VѥS7=u-\%KϦUv,ⳀZ=vkN*+_.ڊ֞iڃ=w @lmr>Oo,VԲɝz &:'45!9pI 0@I[PU""sInvR>A9t$3/|k8yiE c8E!Q\ۂ} %Af4s*A8A΀>D=5uwjnG z?2Q/I=fH4n]澀YmG"2PEHfvZn<šPiA_q/PDտ $$~%NyhrOdM\-m(@\#ƼNJO>a+ uJ*(%¢FPJW,$))} B\_wV] 0TOCÊQ}5{Ho*;;葞rǨMc54S : M7(kY:z`gp Jstˉv'eG^~iD16dA @'N ֭N.?f…1bzJD V o@7R@6<%IF0mj= [}Nۊ57pyv4@<mЭ9Tp?R70қQG[jzib~/)wC? רa-/Cn.ĕH j63pKrhXIƎj o19 f\~:-ѓK47BY̆y%DC~em@]%rs4T G-Ug>HOpVB]{9&^6|m _PLLI7ǒi "'T }? 4|[Fǭtu/_y;Z?HK0Wzc#)~.rĥ+B&JG0[.ΡrOk;VCoX K۝S߳rt:zX\xmJhxNh5 K`;ydp.Ec4XD<-llip.^p: u/.Y[rl_4kz$~Dq]7/T_<菵4K$Ɩ &w S7|K^7MsMGhw㢴0]?fja5aiЦ6C2no• f=)d^v qNcԎl=u]?;f-E~nv}5%Oջd덿=Z%v  nKu ̓*J#1hu1Hr o}SZu=w;nϗU `FȶEn?߫k&l9YdgA8NSGD09MAK{ހK3݊[_]%W4zۈu9\~n3~zir X3k`Psn=m]ԃJksT9deYN`}/]U#b;Rt,lh*#JB+ (iGx\}~IֳFv@Tu֭J @-LwzYgw`wx-(d٢]F3_XcYmQԃWb-F K5d-0b球—֨T+_Zxcj*`}|x~LF*S*oMتAT1p71?R t>R'"Ey)oP7%$rv QeE+nzlVlFrkt''?R'ZCEIKy ga0^}pE;Kq{T/?i"%1ޒb-Ծqƛ˵+ 8]rIڣV{dȪ͜\AQvOS]0.NX9svb?OE~FPU}o[YKrA̓U%7Dw q b/h AhPbQؓJB8I ?I%=XtO;(PhLd S 'hݱ>|TV?,O"\`7.2>D fmg;-C'u, zA`-ټ$x vck2[xp\cbl΀ihsivaÛM,gĨlMz7JvˑVRWϋNo4(-XB^Cl&Vnnn D4[k6N&}f3YQw@$U$(Ǫo:-ZG#&/} ?N}ƥ7A!MhW>?iXprA١b?uϱι-h6;SB#/@ѿJ !%Q)Dq:{JI^ޑˡPY7UG(h?HmъvREH=N`P)QG9FMSMG@2E$Q $s~TkN"9Ն8cF^"?+G٠ ^*gUlFVxUpoC.XCƵ׵͉qK[k[K(l; ӡn%^Rj,$) 1n.G:Cf(,;ĴR—F_~^;իD;6|/jGGSSGGӎļDzbR/X?Up14u$`[ߜH477I~~Irߙs#6+heW6@wK̸h6, 1C"=meA =@z sls];kklr^"s青>&Մ-[{JiҴ9[ݵȩ-]dޢc An۹g}ꒇ6hTɖ?3s^kLcY 1Zn[bݴE߆դwk3f> fMDՠaD ~}&@5u gnOȢ<'` &bӬ-6;X"d*awYvtLXָkUߩa=HR_@+j2T*£%/͸oƤy 19/7 ~7_o+$DүsIH:r yiF:v(dO":omdM8 ;Z9uʩHCg\K/*ԙg*-I_ERqR'[f?GUAovb A$e]/Կo?|ԐQm4G7G833+ 74z*)$݋JpDNj5pqeDf/>%gW{U:g,nlU\t'%E}͝uCꘒܻߺp}U+^b'o(5gVBIOEm>5yzg}AP-P/ޫ6)x5/t;1p1L9Aܳ|)X]mkFEH/4}:,oLMo6]YM50u[yҫfVh?E-A_i﫝j . 6|5`#Z-svfqӟs͚>w7C{ A]Bz,iH'dv?`E x,mz`F[2avhp%(̒ʂ5Ԧ;Gюh\y";|"ٝʖrxzsPHCTvP$ly}iyhvMCr)#x-.(t%fu€(ۅeUUo pqeˡ啗syi Xk`>X@2P. 2͌>n|,/4} ?A&Jr+ɐCV]{Z0- A= F$+%UZyޗٲR B)wT8(aRΣ*-sr5v !^tZ:/K,'F  9=G<Cu"$-FS2(F 0Q+Xw,]=bh[qBQI ;)"Ō926r?}lV =b[j4AzKkQ?T[%$KQ-l_@l/ &;차Dr?P_dE1~z^I~breufP/պ# E+S\G-R4 SSV俑; *`G*5'dL ~ 5Fhb` ꁜ4[b$~GNAX$~ }[W}_z×6m&~O%j/r&|_Sy<-*Lϛ,JQzͤ𫷣|V|GVW~z  HE YnH4r7P?99ߡ|O-5 %4 dzO/4L_PsT>LQD( J8F+)jCb Mu2Xc8$t}&@Qr-֤U_o6q7P1ˤ+rc6I \ (*v24Uc(A ̣93]z;0'=*,e56Va,qh*P@wȬG/Oj|FIm #Pz;Jwʎ}< z Tt~`ȱGP%;? 5((u# vՊI#9,?Gb4K]Qgԟ]E[ phʯG+`Ęp?@>!}" ҽr=CD5 62ZY? iA T(E UJu;"}պ#LcӗVWO&CIԙu8*烞QaQ^*z(L|Jӏ^fp104~CUx*rV*N9π׳Pūsp_L3Z"}&rO|l~kC/Wj><SxMbSg(]J(Z#x\$OC68-f:{Sҳ蚨o4:)Wb"uiuh~d%BAM sWH.gv%4v+=¿ SGϋjWHWu>[B{[uɶs;laziW߭\zC|\fte&ߕ+Bk/t  CM /@S>Tm G`v`?G(,zb" eAAi7QR<"iX:I܋(aV;4R]}^1vԵ7=p|[Jοeµ{)e#ief0KJq"*F#(GjJFhX#шݍk5ERP΋ ^pCeoe:{6۬5͝sƙ8X K6V[=}V+hͧJlZZ5W;TeV-@HID<͙[)֐l^bXeNN"K]@b?.HH gzXaْA}MOeXHNrڟW;htgttOyu3=*פؿCFGsh9JͽZ-k]L-~hii.49Qr5I,Vݓ^jf_},Q6?5NV ޞˍYٜN%ezqƨ>Z Nt1 a %= yhޙ HJZ? hvrk@mY`^insF\*|Lz!/?)(0 MS4(ȗh{-'ho7cCҞ?6'|ubգ@!bÙf{tz1UA?=@ t%䕉iu[ NiD GT@:p<(cXUm2ϱ7zOM^FϴYUfwGs#t:/~Os]Fݑ((^?L$Sʽ WzT>m'_d:5Lh;H7WgzgZZb3{2d5Jj9c+\vqzDbbƶg "l@צpQBbS Q>+d p%}L!cdwHopx(Tpxp#:dvQ qdAQFdLKmPR pU?l zg-jPbGaR&^q>u8p&Ӯф `MGSܵaoWܛZaâٟݰV5Rs2NX qGB OKg BW)Sg\ӡl]z<߲o-_- AKMqӭ!æSigy۰]K;ST'kPqee7cZT{~*7b\H?jٵl3P оwT2jY;)l DueytOTjöUHXgɬ,WϢ^u![]vF| QGh`(# R'5XDQqM6gc'bu:'H( ?yյ6~.e[n *UyZst9R!GMM$xz$]{L<}4JZ~MVՕhy >@u +]2FqO8jѥWCQqrw.䄫ޥ\_y\On)IKGRHŁqI. d+u@ϴ kŤ}9Tv6*xge7?ì}S-AU OMlJ pժݧYwhi6\fAZc,rjFTMj8kO51TqW_n`7%KWsd0:`OXs$4?:SI1W-Pr}² 9.&P^f 8(WI``@5a}ziV pPԽ+:d\j"=aj)W$q{͜p)V|7hj$L֡9\ځn[ k{lG.m m~TEbȭm` wnyP&:PLJY_pNWzVS׃]7Ed%i癬| EWM7r HB6`UGZ 9N2l2ɅHY(ŗiwݓ[`cZR;Yz=TrvH9c. ֲG6*p΅'[:/ҪXCYхMt-']n,{@ cObIN.xN F9뛝NK[Xr=Wm ݏƦY+?sJgXuP%ȗV^[ W;W xvi/XS3ȼ2ԩZ f2/y?8M@Q*˄CXk?MzTy?ZYu׳)]͕1-a7j~ .d  'VztXK2k̹d?zzK.>,BZ`q'kHqy5j>a\C#H;#p7l4} IR7ފ0$=V#_.vs{g>h!Ab/p7=zmi%͟3)^Oj<_UNY63dsIr8EjU* 33|v ;OB@,,\cwd}6k.ukF9'26D]exGJK.׽}S$@ t";2ɩ*41_x7QbjX9Q;#{9eI -奐br B<9dpzIVQ:l+si #=T+R(MDC$ a̱ ONgj19gqXk}FdcG,&..^ɷwwc>E_]3U|t{Jf窂u_.\*W=}lNo+^Ṿ vP>~sTjWz~_ogS}-DTd -TAaYf3,PATcm ռ4g}mE$BwŪ8>9JW⁩O/9PJCXA{,@c,tEJTj98Q& HPl~K%ƞ1ѻ -eD zxNXuz.9}Mc&:Z5ә8% յսmomCB:l8~ܦEjTYHYvnV^IN]]ŽCXkg#s cSB$Ý=$k}cG&/z}_v6<7IVGGg*l\RXST)šE%Yu~Q~>XЅ`9Wk*@_ՊpM]0*%a3X팁KM|{FԔ 췾d7[nlͬD@m8e cż#gHdd@~.jllɛeRcxE(( Km¼GXA7S@[l.%գnMDs]n_Q 5i?zGTG3T@e i,r O2<l+/,%m ۚXn|E]lí[m<|#z+5 7&\5S-{AE^tK M^rq]FmC%2vJ)W-}OM"`9l+=%"T'8zH3QҐѩYP~VزNi 7ۛ ?w1xc`d```d?oAePBYt?;"@.Hc xc`d`` &]aA_x}SJAS<` b)6 >@D"X\o!ι{,_oggg #JVYp>uC4&*<=$g9W@.0q- ;:pt"HUe5 Vg([Ax9!޴EMߗ4N&ӞwjtԞeσLp>w>Gpfz`|^aż>)o oMg+RmRq,RJ1XTN7t{IE\F8U mb:fN&j9Yxc``ЂM /^0Kؘژ0=avcca>bĒIJk ."/ I888qqpnǥ5w)^-8 ||||[5? JPKLpPa) "Z"WDmDWc3K O~/cLuNN+9K8;99/p>"k676-nܷ0h8)iʋK+s9@.xڭNAwh /"TD#J$rqr|!'O3XFާ0wY 1fg;73;3xE0C q=qX4GA$x ZB8ڃ Dw!IaSX w.0?oN؍gڍ@\A`sb k`sݡ},0Ya DȵȵMyFMvYdS20~>/qJG i<#c0C~G9ee Kvв[ڷ{&V(Ө1j1MZqr7,gKܥX0QY{ MYжz=a:[jEݢ BZZ=ns`+ȍxmUSgFB]9I$uw-J;mPwwwwwwwwlޕ]<3)e׿7R^ VV_@$zГ^З~g`0m[czf`(3233 23s2s32  eD*954XXeXX14i++ kk [[ ۲3Qfvd ;1qgg& nLdOboa_c@`PpHhXxNDNdNarsgrgsrsrs rsWrWs rs7r7s rswrwsrOO // oo __ ?? f,˺eݳYϬW;MelP68s䘉GE{RαM 7nܺp;ڛZ[ݛƵ? ѵֵykx~yj?\3V+wE5=QMjzTӣ(vN؉k/셽d/Kd/Kdbbbbbbbbbbjjjjjjjjjj/r{^n/+v ;NaS)ԼffffffffnnnnnnnnnnaaaaaaaChQN-ܩ?C?C?C?C?݇C}>t݇C}C?C?C?C?vNjHMp[qn???????>>=<<<<<:::::::U>::::::::=;;;;;;;;;;;;}VhSoTPmpire-2.10.2/mpire/dashboard/static/fonts/glyphicons-halflings-regular.woff2000066400000000000000000000431541461637447300271510ustar00rootroot00000000000000wOF2Fl\F M?FFTM `r $e6$t 0 "Q?webfe5옏@? t,3+2q FYO&>bm5ZH$Y{H jd Չ %٧y"+@]e{vNc)n?~?萤h_&iѝ?>^K v-cۍ12Ky,'n(3EwiB& Tlh0M҆dYrﲬnti]yurVXsjgMnәHW r2>iT`V7R(+o6'cB4ι㿚T ]a[Qd<3wq8,rTI80>E?*E痦#7'S ocʷ_7&#*+)+4aA6cy٣f(bF$;{ YA1vP-tG"Cf- WԙuKְK#*K< (Z`٫ [%YT{%Ɋ$s{oջvt"p4`ߩϤ}o `'ne> G5sz_N PKӦvmU ɾ{z"3`l W#Ԑ^@+,ckoAOpnuzzJ)Υ1}O=xR`J`qUs/+kv1xljlEl\nDƶVjg{Zdz7 5!xm5o[u&1ڂHBkAqrR (\gh7Ҋy=HZUPh$8RgzgͭN:1u$܅>R]"f7 K^'3+E/^YU5]NB.ʋ8+͏8,|{M|Aua|a˅՝% lKGP,Nukc8mX@d̘?Y&{?P(G]Or-\LF9,&y8r3ܟ?p>~sDz1?\U5q=tzԒ&Znj%mM"}tkDwh-=mB76&:һqt" 1:Еu;"K_/Jdc0l0'^B8VCzg[ ;d Ybȃuu;@*}y| .'C>\g=9VŐ[o|g^ >d 9 *E|A*M[[*mOQz?Pn?R)YoT&[U*5S MB [ oYDh{,}1f?NN ]O/^;\J BEsJrĚ'g/B%o Cn7:|yKt&$s|wP\i]$Z@+ Հ90x]r%+RUEm+ܰ;wu9/I77զQlu\yWN)8ܰvY*umm( fEG8 j#IRz #q߷ )Y$ Лc_%m-{!0-` ;公hyV]Hv! ta\K[1{"j 6@3T0%Θ"ԙZIGS.ΣpӬS1eٓ؛ Yv8d\BlSR)ӆ {Iӆ%>0Ўڦ\'cg2%4QD 0͒3B"MՎ&ۊhIڧRgME I(5UD] }b8$8>X h"l΀j.%ۀHH- Iݸ#1C4Y7YݖV o>P]6O47f ~AJdYF€.oy) 8l 22e1H[t@!ȅ 2\@5ٓ%Zkޒa@.`n3OFR(󅥶ZkLkF HWjY I5*6eSbk.5F,.N0ԙ|V||~N( 4],Jp|~xeA5/ڻSvy?'_v|rXHQēB@= XB94TBBcHP+_YH#$`FB;+BPR4̼ t:t"ZEJ^!XǓq4_dTW(5܀IUŇAz@U6n.WGXHRK&'swMjʎ<3)`#F@  F Ԣvob$x +u&}|X&[٪8F-E&/>/G.az^/})'x$O=<zoA9M؝&~3r3g'8ң\-MDzk5A G9|1-! 87[,mRu|57 =X,aJ^tN4\fЄ]AzH^7F&k"LU>}>rBX(ۂT% JdhKPKTFaA3HHC[r;ad54 lLkjG{8h~ fR@9wB0 zS'a7@@Nƹlbj3hNXF/es'DsQjw}Jz^:V.:ڋ{ͼ(ȲBɦx<Db#"S{PHuN/{r6;wUsPО p8+6g_2lΡ6H džH: dBtGNmx@j |{s9=wR/oDJs5z>;'xEq^r^=G?9AA_K%Dɮ:uikjkIeG՝#*)jm|t}`JZ؈H=4{g߁)qXMA,H71V"o,Y#hݨS_;a_ԗZ^cn4HE?} ȝ٤=}BWvުUehGF;@2S@f n2#fY:]JyH]-G׌wgv'|0e _7Ґn+fٸY<( ?y%wm+j&&!c^u'b&hm6¤*2 ?AIƲ5FWؙ[ƜBUzIE!m:xheǮnz|]% mrUFگ1 };!n F&gP;&$$F).tBQ3(C=Xes;iي@~NΡE SRh\BeobTnΒju g@'qQ딎nx.u6bVU& ];!C_  5*zɺmRQuqPZ0}mn^nOrT:U'h0nZp^R|DF_b\@mDE8{oGM᠜q}Sd C,iܚE/Ë[d8],MCI_u,]Vc"pg@`"y),;B^el2'.(Ęy>-|hw;jՍiԽ_o|!@)ɢ=̌SPz*!z})|ƧT}jEtCZný*՞4ۆ׽[ 9Юݓz`Wmeo|j8j59@.EV/ZW@|f_\"${v/;a:Sei3TG*]ơ/h2C32$1}DNXt?Fϝ~n,Pj9.>ף{ 9EN-v|3hCиE XT;P$=J-gݕigz~q(A<:h193N̽Q}CLWߧ׎~ b"|4u}cy62[ \d,ҎճbkD%0Tx{=;Է(i LS13Nh/6?'E^~P{sZZKĞB{Dt&z)Uoa5Q3ȗr~ F]$<tm(} MB@[GxFh8#},#u Laz(Qh4%xm`Uչ.Ev1a4_'/[d{FxI59 D<&8VEFg 芘#I䟍2S_]QqAn_Q>bޘ4g-0&E#ci8 vR/4rP7KsOWN3ՏvE\bqQ5ZڽVy5]h/ i)-/kNю#e)"P {KSQx>a&, _g-mc<n]Ч-52cz 7d PzVOPvfR Rఓ9Z -dC`,at=k?v4#P Bإ/[s.-bH)ɺz '}׶w!rXZ .:Vn;->: 6rUcs4kVW{#5ߑ0B`ܝ0u".QdB0Cr]#Q9lqN^ֳh~NU\ 16 ~SnTl\THҲڛ-~G~)$oQ7-C}q%/avO|[q4~Bc-$N76w{V餃.&(o*n NeRi4!3R"4nbm-y[X."!QKE\N4gՠםaNp >k)90BZBs yrer)vDtrv\v[>rJm a̼~uՏ>rMZcB<`)\yt|ۍr'<>[Îh7Z8caI! p⢟̮,G k5@`iw nО8pv *'O A[.rhT pR?+;\*HsLqUf:ql-ć *6!h+ˬ{h- jgkMMP#:}{/VŶC]옙&[W$ګ^#4fWa\ 5躺M[6)T3~ :. Z`si(RQ|/` il^L#f-;-C;_*{@EMCooÂ_7TrqzF%ׯ|UEƫUs^ݜv{fQ<ĐVPTfͦ?mpP*&QG{cJEPe2)xP0AMɪZHj"׻"AC+zqmVzᖞU%C:@1W [y)J@ob% jA>)Nǀi$At`>?f0gH36p6D|M 4N 4JJڃ jƇ\ p38Я6pV?:$sDNƹ2n,HO\[ոK-)W~im?T:޺UeY-#dJe)Z5?$\dW<,Ɇ;ط5SոTT̄f(PYv=Q ~DX*8辩s- ˨΀55 XRl QC l|5{ӦT\t꼕+en۸Psl3UO[ZS3*,:ÛZLS'̵**@ı~xgno2- WV;pZ9?~$6҄xJ>\QA_Cihbl] 64*A˯ɰqX7YX.-ոaɇVhiKgqNRĆN(r']%٘@3̀jZJ.;nm,S0xͻOF33ҧ<$'GE+}'1f3y5/&Z\RB7dm]8\3߂Ȫ@oT3eu^W@e7l!B,s1$Z&?dC (YЦSm>J"&pt܈P㇄BF4G5 t^Ć$j-a㠍g^ʐCAsT=kTS,|r9IBϘЬ'vGA@thQNj&T=xt;2]P|T- LÞe1ݽWZŚ*MrH5?=o"9K5='k-*AE| qҔ_?\7%|M6f++S*}W_]3fmܮ˳m w!.R#鬪;qq71$•ݙկ_iK&JάMemV5P0> Q5WHIh&4ҍIlE7}sm[cȾ|d^ %Uv1D>.T7*=tZ_㟾1Х:=0pZ6ҋNt(uƝ; B]$kڌ.{F*/UZN砦|oqKG;^侞9NexK \wh~ZpHb䉸 [k8k.bX.QXpxYa^"#Bwnbum5F~>8bN:p4 [gv^ BFUz)?60F8/2C8>N8G%l%5FH{46h4%# 7x oN t\'Ȩ E0#jNãVӹd?WlcW žֵu-}22EN}#䵵2H^a3rqs-S3&f퇣fwl.=W8,cHjcTWנs90ZDMC2ZMdjt"8:g{.Ʊ1Fb618"yԦ> W9 V `jT򔔑r,ni d qN .g+ S Q KaB?_QE rjh>Eӛ;C׭7^q `Ue#-;oJċԝ>) ;Jg׭9R;OgiI7}8Kہqjeؓ+ٗ'nϷk3eFρ0V#pMAzb^PVu~1uғwn ^.II_vdW[Q,+Lbćq 9V} ΏVw4qU3&jıHYb ttT7ρarBwP9?)uT/aA19kM \Psq+=[5͔?9W+^o^E8s)f 2aQxi& NE>"^Naa;f9]NE& t^CLz'e8ZRs&67_ãcyJ1 @TZ?SD2 |POӌ\dR7zH9iQ#zrc.4GR4qx<2~Xhnੳ2auBNC+kX0 aj5n>މe3vާ<>_ uH:XR%~9!4oѼ38? 1d#A&{A!i6 /Xa㇤=W;|) g~ ?*悽 }ڧKt>5|E.A Q6 (6 6є7<9_C f1Ўi8, V4$uti,.`v6r P gFBɎ t C3; ,oÂx| /KMp1S_X.fV#U>Ȓ#B] AIVoІϵGTV1nr+OXS% ³fOZ[_9P߰ {Gln%#hdwH= ye/W>,IP,*MV~ºK&eċM콣=)qFS"GTF*LX,h[wweWQEx ?{^چExhiׂJH|^͓e*^Я.uxEb#;ԝ<]z]\wNhochqE=4Q17W̓lÕ6᧿HE_̣qy YR۫9~l4sVy`Uߛ,#_u+DeM~hq벇#Yz$; 5ͯ9$ z> *jO$$O/xRtf-}*oɦ|3M;xިUl/.~XǎY4x3&x";$KI5dڭ ~w[M9O%4Q}S^t@w[Y;-s;bwH-* imI-1e/~TNN.p)H$W~ƦO (9, ]gM6r+#%/swA$q4O> d9}+$s?0a,>yڈs<=,c_*\D}2MT8/4g'ڦ8'}"C*\9#Y>z$7c[s|"$} ymzQx 5%o$jkp)x-:И|?ofgFr2SZq}q o,wyOgCF1l'L5T33yM92"s5uD6-JUbs O)wR -2/5frϛf@=BFCB&'F}@&yubC?'S49+ÓCIî+f/RU C Fu:C*} T:}{ݽⲷue[!>? ڸ"M 8gz0\HkZ:h~@+#N fjyio!B R'5>`[!T`mC Iѝ}n >W!M}Uav43)!kcȂm? dwv!ה;Xϡۨ}8vt"Ӽ# kvXJ[l[ZݙMÀXC3l[ TaVjʻѬ"œ t:(<cZveQTqHi{銀Q埓'ÖiP■mKAIBF =Tᅽ(&TS?/؁A:ַОV(@wFa^]o]*99Ri_2vM`Pf{QYH#V7v7Ұq>@~uɘ׆Ax/xB3Ġtyb0nG` EDٍA: PwI7nW2ED}.(h"U]9Ih_V@GZ0C pb :L 3tN*N 2!3 Cayn.ɋW`̳}QBCi 8*{57O#aTBUoi0 _^ ChrU}~rL 1z>..=%GG o EuPPsؘ޸8Pu&;*|i&Pbțh;[|y*cVhҼ(~_AqU2GIQ3`^v=@K'ЇZ#4sJ=:sY sڥbyj S_E܃"@~>86#y[cSŬ#SJGZyvvSя扝pwaT/, 9'Jkv%%.~o[ 衧RBjSȀ*$'腁pçSu +9\_f+8u\,tpэkخJ0h(]NQvW7 86:ݣ WcY_i>"R(e]6RA%U6&F]7@̳k3X h?KQ2Bk[?..KKAb65ke+]FeWHU0Oק5 e3Hco>l]02cH9{Z {sO!A,7?ŷ3w俎A Fj8B&8U$G$Y5FL5n1> q2.6e +@/kb{(7i={l͍݂濦81g(%h/EfMҍt5̼vgo ~ਜ਼WKi父UأݖwRSEFT% `=|*=1*SX^w)lfQH(YSSˌK1W]f7ך^&p@T'.%3 5zaTf6A5LX̡|L-ηTg{A)F."hjA;.~o% G#}&]׾c`ChH9xnNY lc\+v\EƧ1D9KX)2b.NWQש$/|6tð32ԛ72иyu0e)Nuh'd~xY ># b"k3 :9v$ПC:)H> զz;ed\jmfOa%9cKxۥ!k%HDn{Y"{n_} )9= _/Z(>lYVgQ#߭:Qbw$zwٮ#U?|Ghz{o$wϜ)|Vh? ZV7%Go/׆E"KӲlp76-z !l4n>$\zV?szqejQ]m^=^ !lHB4sL i9}2^K5OB)O v^~݀xrm\K&G^5CL}&FB]Kn3|sGjykObsܽaW?R6Jfh2 lBS\=jV*Y^˺^E)*\ rr(a@6nԌ?}dLgIvqNcaƮkmLcA!hdVwc=憖s_:җsLg>1*4-%&0Ub)Eܬ*b51 ++;<`!qfM*,[/GK+{,>CLR%%c~'EGAG=h䟔8:IDN)W̻AF)ucw'qhXèL@a~6Pc2L"A2bU & 9A#QLO:E9kfKFb93tL$cˬpLz5dp۰>$`.~X=?NͰ/LPNo0p b8AR4r Jj} Ӳ04ˋquۏAFP 'HfXDIVTM7Lv\(N,/ʪnڮi^m?~ QU Ӳ04ˋquۏb$tV&gϖr>t?0:t>s.max?s.max:t)}function s(i){var s=h(),n=s._rgba=[];return i=i.toLowerCase(),f(l,function(t,o){var a,r=o.re.exec(i),l=r&&o.parse(r),h=o.space||"rgba";return l?(a=s[h](l),s[c[h].cache]=a[c[h].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],h=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=h.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,a,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,l],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),o=c[n],a=0===this.alpha()?h("transparent"):this,r=a[o.cache]||o.to(a._rgba),l=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],h=s[o],c=u[n.type]||{};null!==h&&(null===a?l[o]=h:(c.mod&&(h-a>c.mod/2?a+=c.mod:a-h>c.mod/2&&(a-=c.mod)),l[o]=i((h-a)*e+a,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),l=Math.min(s,n,o),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-o)/h+360:n===r?60*(o-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[a]&&(this[a]=l(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[a]=d,n):h(d)},f(o,function(e,i){h.fn[e]||(h.fn[e]=function(n){var o,a=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=h(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(n),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,n,a={};for(s in i)n=i[s],e[s]!==n&&(o[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(a[s]=n));return a}var s=["add","remove","toggle"],o={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(n.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var l=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",h=l.children?a.find("*").addBack():a;h=h.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),h=h.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function n(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function o(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function a(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(s)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,i){for(var s=0,n=i.length;n>s;s++)null!==i[s]&&t.data(e+i[s],t[0].style[i[s]])},restore:function(t,i){for(var s,n=0,o=i.length;o>n;n++)null!==i[n]&&(s=t.data(e+i[n]),t.css(i[n],s))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(i,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(i)||"",t.removeData(i)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(i){var s,n=i.css("position"),o=i.position();return i.css({marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()),/^(static|relative)/.test(n)&&(n="absolute",s=t("<"+i[0].nodeName+">").insertAfter(i).css({display:/^(inline|ruby)/.test(i.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight"),"float":i.css("float")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()).addClass("ui-effects-placeholder"),i.data(e+"placeholder",s)),i.css({position:n,left:o.left,top:o.top}),s},removePlaceholder:function(t){var i=e+"placeholder",s=t.data(i);s&&(s.remove(),t.removeData(i))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function e(e){function n(){l.removeData(s),t.effects.cleanUp(l),"hide"===i.mode&&l.hide(),r()}function r(){t.isFunction(h)&&h.call(l[0]),t.isFunction(e)&&e()}var l=t(this);i.mode=u.shift(),t.uiBackCompat===!1||a?"none"===i.mode?(l[c](),r()):o.call(l[0],i,n):(l.is(":hidden")?"hide"===c:"show"===c)?(l[c](),r()):o.call(l[0],i,r)}var i=n.apply(this,arguments),o=t.effects.effect[i.effect],a=o.mode,r=i.queue,l=r||"fx",h=i.complete,c=i.mode,u=[],d=function(e){var i=t(this),n=t.effects.mode(i,c)||a;i.data(s,!0),u.push(n),a&&("show"===n||n===a&&"hide"===n)&&i.show(),a&&"none"===n||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!o?c?this[c](i.duration,h):this.each(function(){h&&h.call(this)}):r===!1?this.each(d).each(e):this.queue(l,d).queue(l,e)},show:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(o(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):a(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,l=o?a.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=a(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}(),t.effects});mpire-2.10.2/mpire/dashboard/static/jquery.min.js000066400000000000000000002541211461637447300217250ustar00rootroot00000000000000/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="
",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; } // convert a number to a color using hsl function numberToColorHsl(i) { // as the function expects a value between 0 and 1, and red = 0° and green = 120° // we convert the input to the appropriate hue value var hue = i * 1.2 / 3.6; // we convert hsl to rgb (saturation 100%, lightness 50%) var rgb = hslToRgb(hue, 1, .7); // we format to css value and return return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; } // Hide part of a text if it's too long and add read more/read less functionality function AddReadMore(tag_id, char_limit, text) { // Only update when the text changes. We strip the ' ... Read more'/' ... Read less' parts (14 characters) var original_text = $("#" + tag_id).text(); if (original_text.substring(0, original_text.length - 14) == text) return; if (text.length > char_limit) { var first_part = text.substring(0, char_limit); var second_part = text.substring(char_limit, text.length); var new_html = first_part + " " + "... Read more"; } else { var new_html = text; } $("#" + tag_id).html(new_html); } // Refresh contents function refresh() { $.getJSON($SCRIPT_ROOT + '/_progress_bar_update', {}, function(data) { var i, worker_id, worker_prefix, task_idx, task_prefix; for (i = 0; i < data.result.length; i++) { var pb = data.result[i]; var is_new = false; // Check if progress-bar exists if ($('#pb_' + pb.id).length == 0) { // If not, request new HTML for progress bar and prepend it to table $.getJSON($SCRIPT_ROOT + '/_progress_bar_new', {pb_id: pb.id, has_insights: !$.isEmptyObject(pb.insights)}, function(new_data) { $('#progress-table > tbody').prepend(new_data.result); }); is_new = true; } // If it's already completed, do nothing, except when this is a new progress bar (e.g., when refreshed) or // when the success status has changed if (pb.id in completed_pb_ids && completed_pb_ids[pb.id] === pb.success && !is_new) { continue; } // Set new progress update_progress_bar(pb.id, pb.percentage); $('#pb_' + pb.id + '_n').text(pb.n); $('#pb_' + pb.id + '_total').text(pb.total); $('#pb_' + pb.id + '_started').text(pb.started); $('#pb_' + pb.id + '_duration').text(pb.duration); $('#pb_' + pb.id + '_remaining').text(pb.remaining); $('#pb_' + pb.id + '_finished').text(pb.finished); // Set insights, if available if (!$.isEmptyObject(pb.insights)) { $('#pb_' + pb.id + '_insights_total_start_up_time').text(pb.insights['total_start_up_time']); $('#pb_' + pb.id + '_insights_start_up_time_mean').text(pb.insights['start_up_time_mean']); $('#pb_' + pb.id + '_insights_start_up_time_std').text(pb.insights['start_up_time_std']); $('#pb_' + pb.id + '_insights_start_up_ratio').text((pb.insights['start_up_ratio'] * 100.).toFixed(2)) .css('color', numberToColorHsl(1.0 - pb.insights['start_up_ratio'])); $('#pb_' + pb.id + '_insights_total_init_time').text(pb.insights['total_init_time']); $('#pb_' + pb.id + '_insights_init_time_mean').text(pb.insights['init_time_mean']); $('#pb_' + pb.id + '_insights_init_time_std').text(pb.insights['init_time_std']); $('#pb_' + pb.id + '_insights_init_ratio').text((pb.insights['init_ratio'] * 100.).toFixed(2)) .css('color', numberToColorHsl(1.0 - pb.insights['waiting_ratio'])); $('#pb_' + pb.id + '_insights_total_waiting_time').text(pb.insights['total_waiting_time']); $('#pb_' + pb.id + '_insights_waiting_time_mean').text(pb.insights['waiting_time_mean']); $('#pb_' + pb.id + '_insights_waiting_time_std').text(pb.insights['waiting_time_std']); $('#pb_' + pb.id + '_insights_waiting_ratio').text((pb.insights['waiting_ratio'] * 100.).toFixed(2)) .css('color', numberToColorHsl(1.0 - pb.insights['waiting_ratio'])); $('#pb_' + pb.id + '_insights_total_working_time').text(pb.insights['total_working_time']); $('#pb_' + pb.id + '_insights_working_time_mean').text(pb.insights['working_time_mean']); $('#pb_' + pb.id + '_insights_working_time_std').text(pb.insights['working_time_std']); $('#pb_' + pb.id + '_insights_working_ratio').text((pb.insights['working_ratio'] * 100.).toFixed(2)) .css('color', numberToColorHsl(pb.insights['working_ratio'])); $('#pb_' + pb.id + '_insights_total_exit_time').text(pb.insights['total_exit_time']); $('#pb_' + pb.id + '_insights_exit_time_mean').text(pb.insights['exit_time_mean']); $('#pb_' + pb.id + '_insights_exit_time_std').text(pb.insights['exit_time_std']); $('#pb_' + pb.id + '_insights_exit_ratio').text((pb.insights['exit_ratio'] * 100.).toFixed(2)) .css('color', numberToColorHsl(1.0 - pb.insights['waiting_ratio'])); for (worker_id = 0; worker_id < pb.insights['n_completed_tasks'].length; worker_id++) { worker_prefix = '#pb_' + pb.id + '_insights_worker_' + worker_id; $(worker_prefix + '_tasks_completed').text(pb.insights['n_completed_tasks'][worker_id]); $(worker_prefix + '_start_up_time').text(pb.insights['start_up_time'][worker_id]); $(worker_prefix + '_init_time').text(pb.insights['init_time'][worker_id]); $(worker_prefix + '_waiting_time').text(pb.insights['waiting_time'][worker_id]); $(worker_prefix + '_working_time').text(pb.insights['working_time'][worker_id]); $(worker_prefix + '_exit_time').text(pb.insights['exit_time'][worker_id]); } for (task_idx = 0; task_idx < pb.insights['top_5_max_task_durations'].length; task_idx++) { task_prefix = '#pb_' + pb.id + '_insights_task_' + task_idx; $(task_prefix).show(); $(task_prefix + '_duration').text(pb.insights['top_5_max_task_durations'][task_idx]); AddReadMore("pb_" + pb.id + "_insights_task_" + task_idx + "_args", 70, pb.insights['top_5_max_task_args'][task_idx]); } } if (pb.success) { // Success if we're at 100% if (pb.n == pb.total) { $('#pb_' + pb.id).addClass('bg-success'); // Make lightsaber light up if (!(pb.id in completed_pb_ids)) { $('.lightsaber').animate({color: '#00FF00'}, 300).animate({color: '#dc3545'}, 300); } completed_pb_ids[pb.id] = true; } } else { // Danger if we've encountered a failure $('#pb_' + pb.id).addClass('bg-danger'); // Add traceback info $('#pb_' + pb.id + '_traceback').show().text(pb.traceback); // Add a flashing flash $('#pb_' + pb.id + '_flash').fadeIn(200).fadeOut(200).fadeIn(200).fadeOut(200).fadeIn(200); // Make lightsaber light up if (!(pb.id in completed_pb_ids)) { $('.lightsaber').animate({color: '#000000'}, 300).animate({color: '#dc3545'}, 300); } completed_pb_ids[pb.id] = false; } } }); return false; } mpire-2.10.2/mpire/dashboard/static/style.css000066400000000000000000000070671461637447300211450ustar00rootroot00000000000000body { margin: 40px; } h1 { margin-bottom: 40px; } h1 .username { font-size: 0.4em; vertical-align: middle; cursor: help; } h1 .username_brackets { margin-left: 0.3em; margin-right: 0.3em; color: rgb(0, 255, 255); } h1 .username_at { margin-left: 0.1em; margin-right: 0.1em; } #menu-top-right { float: right; } #menu-top-right > div, #menu-top-right > a { display: inline-block; margin-left: 10px; } .mpire { position: fixed; bottom: 0; right: 40px; z-index: -99; font-size: 60%; color: #6c757d; } .lightsaber { color: #dc3545; } .pb_container { width: 100%; height: 18px; border-radius: .25rem; overflow: hidden; background-color: #FFF; } .pb { height: 100%; background-color: #007bff; text-align: center; } .pb_details_left_filler { float: left; width: 3%; height: 1em; } .pb_details_right { overflow: hidden; margin-top: 6px; padding-right: 2em; } .clickable { cursor: pointer; } td.pb_details { padding: 0; background-color: rgba(255, 255, 255, .025); } td.pb_details > div { display: none; padding: 12px 12px 24px 12px; } .separator { display: flex; align-items: center; text-align: center; font-size: 1.05em; margin-top: 10px; margin-bottom: 20px; } .separator::before, .separator::after { content: ''; border-bottom: 1px solid rgba(255, 255, 255, .2); } .separator::before { flex: 0.025; } .separator::after { flex: 0.975; } .separator:not(:empty)::before { margin-right: 1em; } .separator:not(:empty)::after { margin-left: 1em; } .separator.clickable:hover { color: rgb(255, 235, 156); } .separator.clickable:hover::before, .separator.clickable:hover::after { border-bottom: 1px solid rgba(255, 255, 255, 0.6); } .insights { display: none; } .insights p.info { color: #ccc; margin: 1em 0; } .insights span.info { color: rgb(0, 255, 255); } .insights span.clickable { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; word-break: normal; color: rgb(255, 255, 255); } .insights span.clickable:hover { color: rgb(255, 235, 156); } .insights-left { float: left; width: 48%; } .insights-middle { float: left; width: 4%; height: 1em; } .insights-right { float: left; width: 48%; } .insights table { margin-bottom: 20px; } .insights th, .insights td { padding: 0.5em 1.0em; } .insights td { text-align: right; } .insights_table { width: 100%; table-layout: fixed; } .insights_table tr th:first-child, .insights_table tr td:first-child { width: 20px; } .insights_table tr th:nth-child(2), .insights_table tr td:nth-child(2) { width: 100px; text-align: right; } .insights_table tr th:nth-child(3), .insights_table tr td:nth-child(3) { text-align: left; word-break: break-all; } .insights_table .code { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; color: rgb(0, 255, 255); } .glyphicon { display: none; } code { margin-left: 0.4em; margin-right: 0.4em; color: rgb(0, 255, 255); } p { margin-bottom: 0.4em; } .traceback { display: none; color: rgb(255, 100, 100); border-top: 1px dashed; margin-top: 20px; padding-top: 20px; word-break: break-all; white-space: pre-wrap; } .hidden { display: none; }mpire-2.10.2/mpire/dashboard/templates/000077500000000000000000000000001461637447300177705ustar00rootroot00000000000000mpire-2.10.2/mpire/dashboard/templates/index.html000066400000000000000000000032761461637447300217750ustar00rootroot00000000000000 MPIRE {% include 'menu_top_right.html' %}

MPIRE [{{ username }}@{{ hostname }}]

# Tasks Progress Duration Remaining Started Finished / ETA
{% include 'mpire.html' %} mpire-2.10.2/mpire/dashboard/templates/menu_top_right.html000066400000000000000000000002461461637447300237030ustar00rootroot00000000000000mpire-2.10.2/mpire/dashboard/templates/mpire.html000066400000000000000000000017451461637447300220010ustar00rootroot00000000000000
                       .-.
                      |_:_|
                     /(_Y_)\
                    ( \/M\/ )
 '.               _.'-/'-'\-'._
   ':           _/.--'[[[[]'--.\_
     ':        /_'  : |::"| :  '.\
       ':     //   ./ |oUU| \.'  :\
         ':  _:'..' \_|___|_/ :   :|
           ':.  .'  |_[___]_|  :.':\
            [::\ |  :  | |  :   ; : \
             '-'   \/'.| |.' \  .;.' |
             |\_    \  '-'   :       |
             |  \    \ .:    :   |   |
             |   \    | '.   :    \  |
             /       \   :. .;       |
            /     |   |  :__/     :  \\
           |  |   |    \:   | \   |   ||
          /    \  : :  |:   /  |__|   /|
          |     : : :_/_|  /'._\  '--|_\
          /___.-/_|-'   \  \
                         '-'
mpire-2.10.2/mpire/dashboard/templates/progress_bar.html000066400000000000000000000224541461637447300233550ustar00rootroot00000000000000 {id} - / -
- - - -

Task details

Function: {function_name}, on line {function_line_no} of {user}{function_filename}

Invoked on line {invoked_line_no} of {invoked_filename}, through {invoked_code_context}


                    

Insights (click to expand)

Start up time denotes the time to spin up a worker. Init time is the time a worker spends on the initialization function, when provided. Waiting time is the time a worker needs to wait for new tasks to come in. Working time is the time a worker spends on the task at hand. Exit time is the time a worker spends on the exit function, when provided.

Global stats

TotalMeanStdRatio (%)
Start up time
Init time
Waiting time
Working time
Exit time

Task stats

This section shows the top 5 tasks based on duration and is updated every 2 seconds.

TimeArguments

Worker stats

{insights_workers}
WorkerTasks completed T. start up time T. init time T. waiting time T. working time T. exit time
mpire-2.10.2/mpire/dashboard/utils.py000066400000000000000000000163771461637447300175220ustar00rootroot00000000000000import getpass import inspect import socket from functools import partial from typing import Callable, Dict, List, Sequence, Tuple, Union import types DASHBOARD_FUNCTION_STACKLEVEL = 1 def get_two_available_ports(port_range: Sequence) -> Tuple[int, int]: """ Get two available ports, one from the start and one from the end of the range :param port_range: Port range to try. Reverses the list and will then pick the first one available :raises OSError: If there are not enough ports available :return: Two available ports """ def _port_available(port_nr: int) -> bool: """ Checks if a port is available :param port_nr: Port number to check :return: True if available, False otherwise """ try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', port_nr)) s.close() return True except OSError: return False available_ports = set() for port_nr in port_range: if _port_available(port_nr): available_ports.add(port_nr) break for port_nr in reversed(port_range): if _port_available(port_nr): available_ports.add(port_nr) break if len(available_ports) != 2: raise OSError(f"Dashboard Manager Server: there are not enough ports available: {port_range}") return tuple(sorted(available_ports)) def get_stacklevel() -> int: """ Gets the stack level to use when obtaining function details (used for the dashboard) :return: Stack level """ return DASHBOARD_FUNCTION_STACKLEVEL def set_stacklevel(stacklevel: int) -> None: """ Sets the stack level to use when obtaining function details (used for the dashboard) :param stacklevel: Stack level """ global DASHBOARD_FUNCTION_STACKLEVEL DASHBOARD_FUNCTION_STACKLEVEL = stacklevel def get_function_details(func: Callable) -> Dict[str, Union[str, int]]: """ Obtain function details, including: - function filename - function line number - function name - invoked from filename - invoked from line number - invoked code context :param func: Function to call each time new task arguments become available. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :return: Function details dictionary """ # Get the frame in which the pool.map(...) was called. We obtain the current stack and skip all frames which # involve the current mpire module. If the desired stack level is higher than 1, we continue until we've reached # the desired stack level. We then obtain the code context of that frame. invoked_frame = None stacklevel = 0 for frame_info in inspect.stack(): if frame_info.frame.f_globals['__name__'].split('.')[0] != 'mpire' or stacklevel > 0: invoked_frame = frame_info stacklevel += 1 if stacklevel == DASHBOARD_FUNCTION_STACKLEVEL: break # Obtain proper code context. Usually the last line of the invoked code is returned, but we want the complete # code snippet that called this function. That's why we increase the context size and need to find the start and # ending of the snippet. A context size of 10 should suffice. The end of the snippet is where we encounter the # line found when context=1 (i.e., what is returned in invoked_frame.code_context). The start is where we see # something along the lines of `.[i]map[_unordered](`. code_context = inspect.getframeinfo(invoked_frame.frame, context=10).code_context if code_context is not None: code_context = code_context[:code_context.index(invoked_frame.code_context[0]) + 1] code_context = find_calling_lines(code_context) invoked_line_no = invoked_frame.lineno - (len(code_context) - 1) code_context = ' '.join(line.strip() for line in code_context) else: invoked_line_no = 'N/A' if isinstance(func, partial): # If we're dealing with a partial, obtain the function within func = func.func elif hasattr(func, '__call__') and not isinstance(func, (type, types.FunctionType, types.MethodType)): # If we're dealing with a callable class instance, use its __call__ method func = func.__call__ # We use a try/except block as some constructs don't allow this. E.g., in the case the function is a MagicMock # (i.e., in unit tests) these inspections will fail try: function_filename = inspect.getabsfile(func) function_line_no = func.__code__.co_firstlineno function_name = func.__name__ except: function_filename = 'n/a' function_line_no = 'n/a' function_name = 'n/a' # Obtain user. This can fail when the current uid refers to a non-existing user, which can happen when running in a # container as a non-root user. See https://github.com/sybrenjansen/mpire/issues/128. try: user = getpass.getuser() except KeyError: user = "n/a" # Populate details func_details = {'user': f'{user}@{socket.gethostname()}', 'function_filename': function_filename, 'function_line_no': function_line_no, 'function_name': function_name, 'invoked_filename': invoked_frame.filename, 'invoked_line_no': invoked_line_no, 'invoked_code_context': code_context} return func_details def find_calling_lines(code_context: List[str]) -> List[str]: """ Tries to find the lines corresponding to the calling function :param code_context: List of code lines :return: List of code lines """ # Traverse the lines in reverse order. We need a closing bracket to indicate the end of the calling function. From # that point on we work our way backward until we find the corresponding opening bracket. There can be more bracket # groups in between, so we have to keep counting brackets until we've found the right one. n_parentheses_groups = 0 found_parentheses_group = False inside_string = False inside_string_ch = None line_nr = 1 for line_nr, line in enumerate(reversed(code_context), start=1): for ch in reversed(line): # If we're inside a string keep ignoring characters until we find the closing string character if inside_string: if ch == inside_string_ch: inside_string = False # Check if a string has started elif ch in {'"', "'"}: inside_string = True inside_string_ch = ch # Closing parenthesis group elif ch == ')': n_parentheses_groups += 1 found_parentheses_group = True # Starting parenthesis group elif ch == '(': n_parentheses_groups -= 1 # Check if we've found the corresponding opening bracket if found_parentheses_group and n_parentheses_groups == 0: break return code_context[-line_nr:] mpire-2.10.2/mpire/exception.py000066400000000000000000000036131461637447300164160ustar00rootroot00000000000000import re from typing import Any, Dict, Tuple from pygments import highlight from pygments.lexers import Python3TracebackLexer from pygments.formatters import TerminalFormatter ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') class StopWorker(Exception): """ Exception used to kill a worker """ pass class InterruptWorker(Exception): """ Exception used to interrupt a worker """ pass class CannotPickleExceptionError(Exception): """ Exception used when Pickle has trouble pickling the actual Exception """ pass def highlight_traceback(traceback_str: str) -> str: """ Highlight a traceback string in a terminal-friendly way :param traceback_str: The traceback string to highlight :return: The highlighted traceback string """ return highlight(traceback_str, Python3TracebackLexer(), TerminalFormatter()) def remove_highlighting(traceback_str: str) -> str: """ Remove the highlighting from a traceback string Taken from https://stackoverflow.com/a/14693789/4486236. :param traceback_str: The traceback string to remove the highlighting from :return: The traceback string without highlighting """ return ANSI_ESCAPE.sub('', traceback_str) def populate_exception(err_type: type, err_args: Any, err_state: Dict, traceback_str: str) -> Tuple[Exception, Exception]: """ Populate an exception with the given arguments :param err_type: The type of the exception :param err_args: The arguments of the exception :param err_state: The state of the exception :param traceback_str: The traceback string of the exception :return: A tuple of the exception and the original exception """ err = err_type.__new__(err_type) err.args = err_args err.__dict__.update(err_state) traceback_err = Exception(highlight_traceback(traceback_str)) return err, traceback_err mpire-2.10.2/mpire/insights.py000066400000000000000000000307321461637447300162520ustar00rootroot00000000000000import ctypes import math import multiprocessing.context from functools import partial import time from typing import Dict, Optional, List, Tuple from mpire.utils import NonPickledSyncManager, format_seconds class WorkerInsights: """ Worker insights class for profiling the worker start up time, waiting time and working time. When worker init and exit functions are provided it will time those as well. """ def __init__(self, ctx: multiprocessing.context.BaseContext, n_jobs: int, use_dill: bool) -> None: """ Parameter class for worker insights. :param ctx: Multiprocessing context :param n_jobs: Number of workers :param use_dill: Whether dill is used as serialization library """ self.ctx = ctx self.n_jobs = n_jobs self.use_dill = use_dill # Whether insights have been enabled or not self.insights_enabled = False # Multiprocessing Manager for storing the max_task_args in. A lock is needed to ensure no race conditions occur self.insights_manager = None self.insights_manager_lock = None # Timestamp indicating at what time the Worker instance was created and started self.worker_start_up_time = None # Array object which holds the total number of seconds the workers take to start up self.worker_init_time = None # Array object which holds the total number of completed tasks per worker self.worker_n_completed_tasks = None # Array object which holds the total number of seconds the workers have been idle self.worker_waiting_time = None # Array object which holds the total number of seconds the workers are executing the task function self.worker_working_time = None # Array object which holds the total number of seconds the workers take to run the exit function self.worker_exit_time = None # Array object which holds the top 5 max task durations in seconds per worker self.max_task_duration = None # Manager.List object which holds the top 5 task arguments (string) for the longest task per worker self.max_task_args = None def reset_insights(self, enable_insights: bool) -> None: """ Resets the insights containers :param enable_insights: Whether to enable worker insights """ if enable_insights: # We need to use a special wrapper which sets the manager to None when pickled. For some reason Python # won't use the __getstate__/__setstate__ of this class when passing the object to a worker, so we move # the logic to the wrapper instead. self.insights_manager = NonPickledSyncManager(self.use_dill) self.insights_manager.start() self.insights_manager_lock = self.ctx.Lock() self.worker_start_up_time = self.ctx.Array(ctypes.c_double, self.n_jobs, lock=False) self.worker_init_time = self.ctx.Array(ctypes.c_double, self.n_jobs, lock=False) self.worker_n_completed_tasks = self.ctx.Array(ctypes.c_int, self.n_jobs, lock=False) self.worker_waiting_time = self.ctx.Array(ctypes.c_double, self.n_jobs, lock=False) self.worker_working_time = self.ctx.Array(ctypes.c_double, self.n_jobs, lock=False) self.worker_exit_time = self.ctx.Array(ctypes.c_double, self.n_jobs, lock=False) self.max_task_duration = self.ctx.Array(ctypes.c_double, self.n_jobs * 5, lock=False) self.max_task_args = self.insights_manager.list([""] * self.n_jobs * 5) else: self.insights_manager = None self.insights_manager_lock = None self.worker_start_up_time = None self.worker_init_time = None self.worker_n_completed_tasks = None self.worker_waiting_time = None self.worker_working_time = None self.worker_exit_time = None self.max_task_duration = None self.max_task_args = None self.insights_enabled = enable_insights def get_max_task_duration_list(self, worker_id: int) -> Optional[List[Tuple[float, str]]]: """ Initialize insights for a specific worker :param worker_id: worker ID """ if self.insights_enabled: # Local worker insights container that holds (task duration, task args) tuples, sorted for heapq. We use a # local container for each worker as to not put too big of a burden on interprocess communication with self.insights_manager_lock: return (list(zip(self.max_task_duration[worker_id * 5:(worker_id + 1) * 5], self.max_task_args[worker_id * 5:(worker_id + 1) * 5])) if self.max_task_duration is not None else None) def update_start_up_time(self, worker_id: int, start_time: float) -> None: """ Update start up time :param worker_id: Worker ID :param start_time: Timestamp """ if self.insights_enabled: self.worker_start_up_time[worker_id] = time.time() - start_time def update_n_completed_tasks(self, worker_id: int) -> None: """ Increment the number of completed tasks for this worker :param worker_id: Worker ID """ if self.insights_enabled: self.worker_n_completed_tasks[worker_id] += 1 def update_task_insights(self, worker_id: int, max_task_duration_last_updated: float, max_task_duration_list: Optional[List[Tuple[float, str]]], force_update: bool = False) -> float: """ Update synced containers with new top 5 max task duration + args. Updates every 2 seconds. :param worker_id: Worker ID :param max_task_duration_last_updated: Last updated timestamp :param max_task_duration_list: Local worker insights container that holds (task duration, task args) tuples, sorted for heapq :param force_update: Whether to force the update :return: Last updated timestamp """ now = time.time() if self.insights_enabled and (force_update or (now - max_task_duration_last_updated) > 2): task_durations, task_args = zip(*max_task_duration_list) self.max_task_duration[worker_id * 5 : (worker_id + 1) * 5] = task_durations with self.insights_manager_lock: self.max_task_args[worker_id * 5 : (worker_id + 1) * 5] = task_args max_task_duration_last_updated = now return max_task_duration_last_updated def get_insights(self) -> Dict: """ Creates insights from the raw insight data :return: dictionary containing worker insights """ def argsort(seq): """ argsort, as to not be dependent on numpy, by https://stackoverflow.com/questions/3382352/equivalent-of-numpy-argsort-in-basic-python/3382369#3382369 """ return sorted(range(len(seq)), key=seq.__getitem__) def mean_std(seq): """ Calculates mean and standard deviation, as to not be dependent on numpy """ _mean = sum(seq) / len(seq) _var = sum(pow(x - _mean, 2) for x in seq) / len(seq) _std = math.sqrt(_var) return _mean, _std if not self.insights_enabled: return {} format_seconds_func = partial(format_seconds, with_milliseconds=True) # Determine max 5 tasks based on duration, exclude zero values and args that haven't been synced yet (empty str) sorted_idx = argsort(self.max_task_duration)[-5:][::-1] top_5_max_task_durations, top_5_max_task_args = [], [] for idx in sorted_idx: if self.max_task_duration[idx] == 0: break if self.max_task_args[idx] == "": continue top_5_max_task_durations.append(format_seconds_func(self.max_task_duration[idx])) top_5_max_task_args.append(self.max_task_args[idx]) # Populate total_start_up_time = sum(self.worker_start_up_time) total_init_time = sum(self.worker_init_time) total_waiting_time = sum(self.worker_waiting_time) total_working_time = sum(self.worker_working_time) total_exit_time = sum(self.worker_exit_time) total_time = total_start_up_time + total_init_time + total_waiting_time + total_working_time + total_exit_time insights = dict(n_completed_tasks=list(self.worker_n_completed_tasks), start_up_time=list(map(format_seconds_func, self.worker_start_up_time)), init_time=list(map(format_seconds_func, self.worker_init_time)), waiting_time=list(map(format_seconds_func, self.worker_waiting_time)), working_time=list(map(format_seconds_func, self.worker_working_time)), exit_time=list(map(format_seconds_func, self.worker_exit_time)), total_start_up_time=format_seconds_func(total_start_up_time), total_init_time=format_seconds_func(total_init_time), total_waiting_time=format_seconds_func(total_waiting_time), total_working_time=format_seconds_func(total_working_time), total_exit_time=format_seconds_func(total_exit_time), top_5_max_task_durations=top_5_max_task_durations, top_5_max_task_args=top_5_max_task_args) insights["total_time"] = format_seconds_func(total_time) # Calculate ratio, mean and standard deviation of different parts of the worker lifespan for part, total in (('start_up', total_start_up_time), ('init', total_init_time), ('waiting', total_waiting_time), ('working', total_working_time), ('exit', total_exit_time)): mean, std = mean_std(getattr(self, f'worker_{part}_time')) insights[f'{part}_ratio'] = total / (total_time + 1e-8) insights[f'{part}_time_mean'] = format_seconds_func(mean) insights[f'{part}_time_std'] = format_seconds_func(std) return insights def get_insights_string(self) -> str: """ Formats the worker insights_str and returns a string :return: worker insights_str string """ if not self.insights_enabled: return "No profiling stats available. Try to run a function first with insights enabled ..." insights = self.get_insights() insights_str = ["WorkerPool insights", "-------------------", f"Total number of tasks completed: {sum(insights['n_completed_tasks'])}"] # Format string for parts of the worker lifespan for part in ('start_up', 'init', 'waiting', 'working', 'exit'): insights_str.append(f"Total {part.replace('_', ' ')} time: {insights[f'total_{part}_time']}s (" f"mean: {insights[f'{part}_time_mean']}, std: {insights[f'{part}_time_std']}, " f"ratio: {insights[f'{part}_ratio'] * 100.:.2f}%)") # Add warning when working ratio is below 80% if insights['working_ratio'] < 0.8: insights_str.extend(["", "Efficiency warning: working ratio is < 80%!"]) # Add stats per worker insights_str.extend(["", "Stats per worker", "----------------"]) for worker_id in range(self.n_jobs): worker_str = [f"Worker {worker_id}", f"Tasks completed: {insights['n_completed_tasks'][worker_id]}"] for part in ('start_up', 'init', 'waiting', 'working', 'exit'): worker_str.append(f"{part.replace('_', ' ')}: {insights[f'{part}_time'][worker_id]}s") insights_str.append(' - '.join(worker_str)) # Add task stats insights_str.extend(["", "Top 5 longest tasks", "-------------------"]) for task_idx, (duration, args) in enumerate(zip(insights['top_5_max_task_durations'], insights['top_5_max_task_args']), start=1): insights_str.append(f"{task_idx}. Time: {duration} - {args}") return "\n".join(insights_str) mpire-2.10.2/mpire/params.py000066400000000000000000000355071461637447300157120ustar00rootroot00000000000000import itertools import math import multiprocessing as mp import warnings from dataclasses import dataclass, field from typing import Any, Callable, Dict, Iterable, List, Optional, Sized, Tuple, Type, Union from tqdm import TqdmKeyError from mpire.context import DEFAULT_START_METHOD, RUNNING_MACOS from mpire.tqdm_utils import get_tqdm # Typedefs CPUList = List[Union[int, List[int]]] @dataclass(init=True, frozen=False) class WorkerPoolParams: """ Data class for all :obj:`mpire.WorkerPool` parameters. """ n_jobs: Optional[int] _n_jobs: int = field(init=False, repr=False) cpu_ids: Optional[CPUList] _cpu_ids: CPUList = field(init=False, repr=False) daemon: bool = True shared_objects: Any = None pass_worker_id: bool = False use_worker_state: bool = False start_method: str = DEFAULT_START_METHOD keep_alive: bool = False use_dill: bool = False enable_insights: bool = False order_tasks: bool = False @property def n_jobs(self) -> Optional[int]: return self._n_jobs @n_jobs.setter def n_jobs(self, n_jobs: Optional[int]) -> None: self._n_jobs = n_jobs or mp.cpu_count() @property def cpu_ids(self) -> CPUList: return self._cpu_ids @cpu_ids.setter def cpu_ids(self, cpu_ids: Optional[CPUList]) -> None: self._cpu_ids = self._check_cpu_ids(cpu_ids) def _check_cpu_ids(self, cpu_ids: Optional[CPUList]) -> CPUList: """ Checks the cpu_ids parameter for correctness :param cpu_ids: List of CPU IDs to use for pinning child processes to specific CPUs. The list must be as long as the number of jobs used (if ``n_jobs`` equals ``None`` it must be equal to ``mpire.cpu_count()``), or the list must have exactly one element. In the former case, element x specifies the CPU ID(s) to use for child process x. In the latter case the single element specifies the CPU ID(s) for all child processes to use. A single element can be either a single integer specifying a single CPU ID, or a list of integers specifying that a single child process can make use of multiple CPU IDs. If ``None``, CPU pinning will be disabled. Note that CPU pinning may only work on Linux based systems :return: cpu_ids """ # Check CPU IDs converted_cpu_ids = [] if cpu_ids: if RUNNING_MACOS: warnings.warn("Setting CPU affinity is not supported on MacOS. Ignoring cpu_ids parameter", RuntimeWarning) # Check number of arguments if len(cpu_ids) != 1 and len(cpu_ids) != self.n_jobs: raise ValueError("Number of CPU IDs (%d) does not match number of jobs (%d)" % (len(cpu_ids), self.n_jobs)) # Convert CPU IDs to proper format and find the max and min CPU ID max_cpu_id = 0 min_cpu_id = 0 for cpu_id in cpu_ids: if isinstance(cpu_id, list): converted_cpu_ids.append(cpu_id) max_cpu_id = max(max_cpu_id, max(cpu for cpu in cpu_id)) min_cpu_id = min(min_cpu_id, min(cpu for cpu in cpu_id)) elif isinstance(cpu_id, int): converted_cpu_ids.append([cpu_id]) max_cpu_id = max(max_cpu_id, cpu_id) min_cpu_id = min(min_cpu_id, cpu_id) else: raise TypeError("CPU ID(s) must be either a list or a single integer") # Check max CPU ID if max_cpu_id >= mp.cpu_count(): raise ValueError("CPU ID %d exceeds the maximum CPU ID available on your system: %d" % (max_cpu_id, mp.cpu_count() - 1)) # Check min CPU ID if min_cpu_id < 0: raise ValueError("CPU IDs cannot be negative") # If only one item is given, use this item for all child processes if len(converted_cpu_ids) == 1: converted_cpu_ids = list(itertools.repeat(converted_cpu_ids[0], self.n_jobs)) return converted_cpu_ids @dataclass(init=True, frozen=True) class WorkerMapParams: """ Data class for all :meth:`mpire.WorkerPool.map` parameters that need to be passed on to a worker. """ # User provided functions to call, provided to a map function func: Callable worker_init: Optional[Callable] = None worker_exit: Optional[Callable] = None # Number of (chunks of) jobs a child process can process before requesting a restart worker_lifespan: Optional[int] = None # Progress bar progress_bar: bool = False # Timeout in seconds for a single task, worker_init, and worker_exit task_timeout: Optional[float] = None worker_init_timeout: Optional[float] = None worker_exit_timeout: Optional[float] = None def __eq__(self, other: 'WorkerMapParams') -> bool: """ :param other: Other WorkerMapConfig :return: Whether the configs are the same """ if other.worker_init != self.worker_init or other.worker_exit != self.worker_exit: warnings.warn("You're changing either the worker_init and/or worker_exit function while keep_alive is " "enabled. Be aware this can have undesired side-effects as worker_init functions are only " "executed when a worker is started and worker_exit functions when a worker is terminated.", RuntimeWarning, stacklevel=2) return (other.func == self.func and other.worker_init == self.worker_init and other.worker_exit == self.worker_exit and other.worker_lifespan == self.worker_lifespan and other.progress_bar == self.progress_bar and other.task_timeout == self.task_timeout and other.worker_init_timeout == self.worker_init_timeout and other.worker_exit_timeout == self.worker_exit_timeout) def check_map_parameters(pool_params: WorkerPoolParams, iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int], max_tasks_active: Optional[int], chunk_size: Optional[Union[int, float]], n_splits: Optional[int], worker_lifespan: Optional[int], progress_bar: bool, progress_bar_options: Optional[Dict[str, Any]], progress_bar_style: Optional[str], task_timeout: Optional[float], worker_init_timeout: Optional[float], worker_exit_timeout: Optional[float]) \ -> Tuple[Optional[int], int, Optional[int], bool, Dict[str, Any]]: """ Check the parameters provided to any (i)map function. Also extracts the number of tasks and can modify the ``chunk_size`` and ``progress_bar`` parameters. :param pool_params: WorkerPool config :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker :param iterable_len: Number of elements in the ``iterable_of_args`` :param max_tasks_active: Maximum number of active tasks in the queue. Use ``None`` to not limit the queue :param chunk_size: Number of simultaneous tasks to give to a worker. If ``None`` it will generate ``n_jobs * 4`` number of chunks :param n_splits: Number of splits to use when ``chunk_size`` is ``None`` :param worker_lifespan: Number of chunks a worker can handle before it is restarted. If ``None``, workers will stay alive the entire time. Use this when workers use up too much memory over the course of time :param progress_bar: When ``True`` it will display a progress bar :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param progress_bar_style: Style to use for the progress bar :param task_timeout: Timeout in seconds for a single task :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function :return: Number of tasks, max tasks active, chunk size, progress bar, progress bar options """ # Get number of tasks and check chunk_size and n_splits parameters n_tasks = get_number_of_tasks(iterable_of_args, iterable_len) check_number(chunk_size, 'chunk_size', allowed_types=(int, float), none_allowed=True, min_=1) check_number(n_splits, 'n_splits', allowed_types=(int,), none_allowed=True, min_=1) # If chunk_size and n_splits are not provided, use 64 * n_jobs chunks in total if chunk_size is None: if n_splits is not None and n_tasks is not None: chunk_size = n_tasks / n_splits else: if n_tasks is None: warnings.warn('Failed to obtain length of iterable when chunk size or number of splits is None. Chunk ' 'size is set to 4. Remedy: either provide an iterable with a len() function or specify ' 'iterable_len in the function call', RuntimeWarning, stacklevel=2) chunk_size = 4 else: chunk_size = n_tasks / (pool_params.n_jobs * 64) # Check max_tasks_active parameter. If it is None, we set it to n_jobs * chunk_size * 2 if max_tasks_active is None: max_tasks_active = pool_params.n_jobs * int(math.ceil(chunk_size)) * 2 else: check_number(max_tasks_active, 'max_tasks_active', allowed_types=(int,), none_allowed=True, min_=1) # If worker lifespan is not None or not a positive integer, raise check_number(worker_lifespan, 'worker_lifespan', allowed_types=(int,), none_allowed=True, min_=1) # Check progress bar parameters and set default values progress_bar_options = check_progress_bar_options(progress_bar_options, n_tasks, progress_bar_style) # Timeout parameters can't be negative for timeout_var, timeout_var_name in [(task_timeout, 'task_timeout'), (worker_init_timeout, 'worker_init_timeout'), (worker_exit_timeout, 'worker_exit_timeout')]: check_number(timeout_var, timeout_var_name, allowed_types=(int, float), none_allowed=True, min_=1e-8) return n_tasks, max_tasks_active, chunk_size, progress_bar, progress_bar_options def get_number_of_tasks(iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int]) -> Optional[int]: """ Get the number of tasks to process. If iterable_len is provided, it will be used. Otherwise, if iterable_of_args is a Sized object, len(iterable_of_args) will be used. Otherwise, None will be returned. :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker :param iterable_len: Number of elements in the ``iterable_of_args`` :return: Number of tasks to process """ if iterable_len is not None: return iterable_len if hasattr(iterable_of_args, '__len__'): return len(iterable_of_args) return None def check_number(var: Any, var_name: str, allowed_types: Tuple[Type, ...], none_allowed: bool, min_: Optional[float] = None) -> None: """ Check that a variable is of the correct type and within the allowed range :param var: Variable to check :param var_name: Name of the variable :param allowed_types: Allowed types for the variable :param none_allowed: Whether None is allowed for the variable :param min_: Minimum value for the variable. If None, no minimum value is checked """ if none_allowed and var is None: return if not isinstance(var, allowed_types): raise TypeError(f"{var_name} should be of type {allowed_types}") if min_ is not None and var < min_: # type: ignore raise ValueError(f"{var_name} should be >= {min_}") def check_progress_bar_options(progress_bar_options: Optional[Dict[str, Any]], n_tasks: Optional[int], progress_bar_style: Optional[str]) -> Dict[str, Any]: """ Check that the progress bar options are properly formatted and set some defaults :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param n_tasks: Number of tasks to process :param progress_bar_style: Progress bar style to use :return: Dictionary containing the progress bar options """ # Progress bar options should be a dictionary. Issue a warning for "total" and "leave". progress_bar_options = progress_bar_options or {} if not isinstance(progress_bar_options, dict): raise TypeError("progress_bar_options should be a dictionary") if "total" in progress_bar_options: warnings.warn("The 'total' keyword argument is overwritten by MPIRE. Set the total number of tasks to process " "using the iterable_len parameter", RuntimeWarning, stacklevel=2) if "leave" in progress_bar_options: warnings.warn("The 'leave' keyword argument will be overwritten by MPIRE", RuntimeWarning, stacklevel=2) # We currently do not support the position parameter for rich progress bars. Although this can be implemented by # using a single rich progress bar for all workers and using `add_task`, but this is not trivial to implement. if progress_bar_style == "rich" and "position" in progress_bar_options: raise NotImplementedError("The 'position' parameter is currently not supported for rich progress bars") # Set some defaults and overwrite others # NB We make a copy of the progress bar options, so that if the original dict is reused, redoing this check doesn't # raise warnings due to the "total" and "leave" being added. progress_bar_options = { # defaults "position": 0, "dynamic_ncols": True, "mininterval": 0.1, "maxinterval": 0.5, **progress_bar_options, # overrides "total": n_tasks, "leave": True } # Check if the tqdm progress bar style is valid tqdm = get_tqdm(progress_bar_style) # Check that all progress bar options are properly formatted. We need to do that here, because when an error occurs # within the progress bar handler it will deadlock (it's not technically impossible to do it there, but might as # well do it here) try: tqdm.check_options(progress_bar_options) except (TqdmKeyError, TypeError) as e: raise e from ValueError("There's an error in progress_bar_options. Either one of the parameters doesn't exist " "or it's not properly formatted. See tqdm.tqdm() for details.") return progress_bar_options mpire-2.10.2/mpire/pool.py000066400000000000000000002113111461637447300153650ustar00rootroot00000000000000import logging import os import queue import signal import threading import time from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Sized, Union, Tuple try: import numpy as np NUMPY_INSTALLED = True except ImportError: np = None NUMPY_INSTALLED = False from mpire.async_result import (AsyncResult, AsyncResultType, AsyncResultWithExceptionGetter, UnorderedAsyncExitResultIterator, UnorderedAsyncResultIterator) from mpire.comms import EXIT_FUNC, INIT_FUNC, MAIN_PROCESS, POISON_PILL, WorkerComms from mpire.context import DEFAULT_START_METHOD, RUNNING_WINDOWS from mpire.dashboard.connection_utils import get_dashboard_connection_details from mpire.exception import populate_exception from mpire.insights import WorkerInsights from mpire.params import check_map_parameters, CPUList, WorkerMapParams, WorkerPoolParams from mpire.progress_bar import ProgressBarHandler from mpire.signal import DisableKeyboardInterruptSignal from mpire.tqdm_utils import get_tqdm, TqdmManager from mpire.utils import apply_numpy_chunking, chunk_tasks, set_cpu_affinity from mpire.worker import MP_CONTEXTS, worker_factory logger = logging.getLogger(__name__) class WorkerPool: """ A multiprocessing worker pool which acts like a ``multiprocessing.Pool``, but is faster and has more options. """ def __init__(self, n_jobs: Optional[int] = None, daemon: bool = True, cpu_ids: CPUList = None, shared_objects: Any = None, pass_worker_id: bool = False, use_worker_state: bool = False, start_method: str = DEFAULT_START_METHOD, keep_alive: bool = False, use_dill: bool = False, enable_insights: bool = False, order_tasks: bool = False) -> None: """ :param n_jobs: Number of workers to spawn. If ``None``, will use ``mpire.cpu_count()`` :param daemon: Whether to start the child processes as daemon :param cpu_ids: List of CPU IDs to use for pinning child processes to specific CPUs. The list must be as long as the number of jobs used (if ``n_jobs`` equals ``None`` it must be equal to ``mpire.cpu_count()``), or the list must have exactly one element. In the former case, element `i` specifies the CPU ID(s) to use for child process `i`. In the latter case the single element specifies the CPU ID(s) for all child processes to use. A single element can be either a single integer specifying a single CPU ID, or a list of integers specifying that a single child process can make use of multiple CPU IDs. If ``None``, CPU pinning will be disabled :param shared_objects: Objects to be passed on as shared objects to the workers once. It will be passed on to the target, ``worker_init``, and ``worker_exit`` functions. ``shared_objects`` is only passed on when it's not ``None``. Shared objects will be copy-on-write when using ``fork`` as start method. When enabled, functions receive the shared objects as second argument, depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on from ``iterable_of_args`` :param pass_worker_id: Whether to pass on a worker ID to the target, ``worker_init``, and ``worker_exit`` functions. When enabled, functions receive the worker ID as first argument, depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on from ``iterable_of_args`` :param use_worker_state: Whether to let a worker have a worker state. The worker state will be passed on to the target, ``worker_init``, and ``worker_exit`` functions. When enabled, functions receive the worker state as third argument, depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on from ``iterable_of_args`` :param start_method: Which process start method to use. Options for multiprocessing: ``'fork'`` (default, if available), ``'forkserver'`` and ``'spawn'`` (default, if ``'fork'`` isn't available). For multithreading use ``'threading'``. See https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods for more information and https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods for some caveats when using the ``'spawn'`` or ``'forkserver'`` methods :param keep_alive: When ``True`` it will keep workers alive after completing a map call, allowing to reuse workers :param use_dill: Whether to use dill as serialization backend. Some exotic types (e.g., lambdas, nested functions) don't work well when using ``spawn`` as start method. In such cased, use ``dill`` (can be a bit slower sometimes) :param enable_insights: Whether to enable worker insights. Might come at a small performance penalty (often neglible) :param order_tasks: Whether to provide tasks to the workers in order, such that worker 0 will get chunk 0, worker 1 will get chunk 1, etc. """ # Set parameters self.pool_params = WorkerPoolParams(n_jobs, cpu_ids, daemon, shared_objects, pass_worker_id, use_worker_state, start_method, keep_alive, use_dill, enable_insights, order_tasks) self.map_params = None # type: Optional[WorkerMapParams] # Worker factory self.Worker = worker_factory(start_method, use_dill) # Multiprocessing context if start_method == 'threading': self.ctx = MP_CONTEXTS['threading'] else: self.ctx = MP_CONTEXTS['mp_dill' if use_dill else 'mp'][start_method] # Cache for storing intermediate results. Add result objects for the worker_init and worker_exit functions self._cache: Dict[int, AsyncResultType] = {} AsyncResultWithExceptionGetter(self._cache, MAIN_PROCESS) AsyncResultWithExceptionGetter(self._cache, INIT_FUNC) UnorderedAsyncExitResultIterator(self._cache) # Container of the child processes and corresponding communication objects self._workers = [] self._worker_comms = WorkerComms(self.ctx, self.pool_params.n_jobs, self.pool_params.order_tasks) self._map_running = False # Threads needed for gathering results, restarts, and checking for unexpective deaths and timeouts self._results_handler_thread = None self._restart_handler_thread = None self._timeout_handler_thread = None self._unexpected_death_handler_thread = None self._handler_threads_stop_event = threading.Event() # Progress bar handler, in case it is used self._progress_bar_handler = None # Worker insights, used for profiling self._worker_insights = WorkerInsights(self.ctx, self.pool_params.n_jobs, self.pool_params.use_dill) def pass_on_worker_id(self, pass_on: bool = True) -> None: """ Set whether to pass on the worker ID to the function to be executed or not (default= ``False``). :param pass_on: Whether to pass on a worker ID to the target, ``worker_init``, and ``worker_exit`` functions. When enabled, functions receive the worker ID depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on using ``iterable_of_args`` """ if pass_on != self.pool_params.pass_worker_id: self._worker_comms.reset() self.pool_params.pass_worker_id = pass_on def set_shared_objects(self, shared_objects: Any = None) -> None: """ Set shared objects to pass to the workers. :param shared_objects: Objects to be passed on as shared objects to the workers once. It will be passed on to the target, ``worker_init``, and ``worker_exit`` functions. ``shared_objects`` is only passed on when it's not ``None``. Shared objects will be copy-on-write when using ``fork`` as start method. When enabled, functions receive the shared objects depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on using ``iterable_of_args``` """ if shared_objects != self.pool_params.shared_objects: self._worker_comms.reset() self.pool_params.shared_objects = shared_objects def set_use_worker_state(self, use_worker_state: bool = True) -> None: """ Set whether or not each worker should have its own state variable. Each worker has its own state, so it's not shared between the workers. :param use_worker_state: Whether to let a worker have a worker state. The worker state will be passed on to the target, ``worker_init``, and ``worker_exit`` functions. When enabled, functions receive the worker state depending on other settings. The order is: ``worker_id``, ``shared_objects``, ``worker_state``, and finally the arguments passed on using ``iterable_of_args`` """ if use_worker_state != self.pool_params.use_worker_state: self._worker_comms.reset() self.pool_params.use_worker_state = use_worker_state def set_keep_alive(self, keep_alive: bool = True) -> None: """ Set whether workers should be kept alive in between consecutive map calls. :param keep_alive: When True it will keep workers alive after completing a map call, allowing to reuse workers """ self.pool_params.keep_alive = keep_alive def set_order_tasks(self, order_tasks: bool = True) -> None: """ Set whether to provide tasks to the workers in order, such that worker 0 will get chunk 0, worker 1 will get chunk 1, etc. :param order_tasks: Whether to provide tasks to the workers in order, such that worker 0 will get chunk 0, worker 1 will get chunk 1, etc. """ self.pool_params.order_tasks = order_tasks def _start_workers(self) -> None: """ Spawns the workers and starts them so they're ready to start reading from the tasks queue. """ self._cache[MAIN_PROCESS].reset() self._cache[INIT_FUNC].reset() self._cache[EXIT_FUNC].reset() # Init communication primitives self._worker_comms.init_comms() self._worker_insights.reset_insights(self.pool_params.enable_insights) # Start new workers self._workers = [None] * self.pool_params.n_jobs for worker_id in range(self.pool_params.n_jobs): self._start_worker(worker_id) # Start results listener, restart handler, timeout handler, unexpected death handler self._handler_threads_stop_event.clear() self._results_handler_thread = threading.Thread(target=self._results_handler, daemon=True) self._restart_handler_thread = threading.Thread(target=self._restart_handler, daemon=True) self._timeout_handler_thread = threading.Thread(target=self._timeout_handler, daemon=True) self._unexpected_death_handler_thread = threading.Thread(target=self._unexpected_death_handler, daemon=True) self._results_handler_thread.start() self._restart_handler_thread.start() self._timeout_handler_thread.start() self._unexpected_death_handler_thread.start() def _start_worker(self, worker_id: int) -> None: """ Creates and starts a single worker :param worker_id: ID of the worker :return: Worker instance """ # Disable the interrupt signal. We let the process die gracefully if it needs to with DisableKeyboardInterruptSignal(): # Create worker self._workers[worker_id] = self.Worker( worker_id, self.pool_params, self.map_params, self._worker_comms, self._worker_insights, TqdmManager.get_connection_details(), get_dashboard_connection_details(), time.time() ) self._workers[worker_id].daemon = self.pool_params.daemon self._workers[worker_id].name = f"Worker-{worker_id}" self._workers[worker_id].start() # Pin CPU if desired if self.pool_params.cpu_ids: set_cpu_affinity(self._workers[worker_id].pid, self.pool_params.cpu_ids[worker_id]) def _results_handler(self) -> None: """ Listen for results from the workers and add it to the cache. Note that when ``set`` is called on a result object, the result is automatically removed from the cache. """ while True: results_batch = self._worker_comms.get_results(block=True) for job_id, success, result in results_batch: # Poison pill, stop the listener if isinstance(result, str) and result == POISON_PILL: return try: if success: self._cache[job_id]._set(success=True, result=result) else: err, traceback_err = populate_exception(*result) err.__cause__ = traceback_err # When a worker_init times out, the pool shuts down and we set all tasks that haven't completed # yet to failed job_ids = ((set(self._cache.keys()) - {MAIN_PROCESS, EXIT_FUNC}) if job_id == INIT_FUNC else {job_id}) for _job_id in job_ids: self._cache[_job_id]._set(success=False, result=err) except KeyError: # This can happen if the job has already been removed from the cache, which can occur if the job # has been cancelled, or if the job has been removed from the cache because the timeout has # expired pass def _restart_handler(self) -> None: """ Listen for worker restarts and restart them if needed. """ while not self._worker_comms.exception_thrown() and not self._handler_threads_stop_event.is_set(): for worker_id in self._worker_comms.get_worker_restarts(): # The get_worker_restarts call is blocking, but can unblock when we need to stop (either due to an # exception or because all tasks have been processed). However, a worker could've asked for a restart in # the meantime, so we need to check if we need to stop again if self._worker_comms.exception_thrown() or self._handler_threads_stop_event.is_set(): return # Join worker. This can take a while as the worker could still be holding on to data it needs to send # over the results queue try: self._workers[worker_id].join() except OSError: # This can happen if the worker has already died (e.g., killed by the OS) pass # Start new worker self._worker_comms.reset_worker_restart(worker_id) self._start_worker(worker_id) def _unexpected_death_handler(self) -> None: """ Checks that workers that are supposed to be alive, are actually alive. If not, then a worker died unexpectedly. Terminate signals are handled by workers themselves, but if a worker dies for any other reason, then we need to handle it here. Note that a worker can be alive, but their alive status is still False. This doesn't really matter, because we know the worker is alive according to the OS. The only way we know that something bad happened is when a worker is supposed to be alive but according to the OS it's not. """ while not self._worker_comms.exception_thrown() and not self._handler_threads_stop_event.is_set(): # This thread can be started before the workers are created, so we need to check that they exist. If not # we just wait a bit and try again. for worker_id in range(len(self._workers)): try: worker_died = (self._worker_comms.is_worker_alive(worker_id) and not self._workers[worker_id].is_alive()) except ValueError: worker_died = False if worker_died: # Obtain task it was working on and set it to failed job_id = self._worker_comms.get_worker_working_on_job(worker_id) self._worker_comms.signal_exception_thrown(job_id) err = RuntimeError(f"Worker-{worker_id} died unexpectedly") # When a worker dies unexpectedly, the pool shuts down and we set all tasks that haven't completed # yet to failed job_ids = set(self._cache.keys()) - {MAIN_PROCESS} for job_id in job_ids: self._cache[job_id]._set(success=False, result=err) return # Check this every once in a while time.sleep(0.1) def _timeout_handler(self) -> None: """ Check for worker_init/task/worker_exit timeouts """ def _get_init_config() -> Tuple[str, Optional[float], Callable[[int, float], bool]]: return 'worker_init', self.map_params.worker_init_timeout, self._worker_comms.has_worker_init_timed_out def _get_exit_config() -> Tuple[str, Optional[float], Callable[[int, float], bool]]: return 'worker_exit', self.map_params.worker_exit_timeout, self._worker_comms.has_worker_exit_timed_out def _get_task_config(_job_id) -> Tuple[str, Optional[float], Callable[[int, float], bool]]: try: return 'task', self._cache[job_id]._timeout, self._worker_comms.has_worker_task_timed_out except KeyError: return 'task', None, self._worker_comms.has_worker_task_timed_out while not self._worker_comms.exception_thrown() and not self._handler_threads_stop_event.is_set(): # We're making a shallow copy here to avoid dictionary changes size during iteration errors if ( self.map_params.worker_init_timeout is None and self.map_params.worker_exit_timeout is None and all(job._timeout is None for job in self._cache.copy().values()) ): # No timeouts set, so no need to check time.sleep(0.1) continue for worker_id in range(self.pool_params.n_jobs): # Obtain what the worker is working on and obtain corresponding timeout setting job_id = self._worker_comms.get_worker_working_on_job(worker_id) if job_id == INIT_FUNC: timeout_func_name, timeout_var, has_timed_out_func = _get_init_config() elif job_id == EXIT_FUNC: timeout_func_name, timeout_var, has_timed_out_func = _get_exit_config() else: timeout_func_name, timeout_var, has_timed_out_func = _get_task_config(job_id) # If timeout has expired set job to failed if timeout_var is not None and has_timed_out_func(worker_id, timeout_var): # If we're dealing with a map/init/exit task, send a kill signal to all workers. Otherwise, we're # dealing with an apply task and we only interrupt that one kill_pool = ( job_id in {INIT_FUNC, EXIT_FUNC} or isinstance(self._cache[job_id], UnorderedAsyncResultIterator) ) if kill_pool: self._worker_comms.signal_exception_thrown(job_id) self._send_kill_signal_to_worker(worker_id) # When a worker_init times out, the pool shuts down and we set all tasks that haven't completed yet # to failed err = TimeoutError(f"Worker-{worker_id} {timeout_func_name} timed out (timeout={timeout_var})") job_ids = (set(self._cache.keys()) - {MAIN_PROCESS, EXIT_FUNC}) if job_id == INIT_FUNC else {job_id} for job_id in job_ids: self._cache[job_id]._set(success=False, result=err) if kill_pool: return # Check this every once in a while time.sleep(0.1) def get_exit_results(self) -> List: """ Obtain a list of exit results when an exit function is defined. :return: Exit results list """ return self._cache[EXIT_FUNC].get_results() def __enter__(self) -> 'WorkerPool': """ Enable the use of the ``with`` statement. """ return self def __exit__(self, *_: Any) -> None: """ Enable the use of the ``with`` statement. Gracefully terminates workers, if there are any """ self.terminate() def map(self, func: Callable, iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int] = None, max_tasks_active: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, worker_lifespan: Optional[int] = None, progress_bar: bool = False, concatenate_numpy_output: bool = True, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None, progress_bar_options: Optional[Dict[str, Any]] = None, progress_bar_style: Optional[str] = None) -> Any: """ Same as ``multiprocessing.map()``. Also allows a user to set the maximum number of tasks available in the queue. Note that this function can be slower than the unordered version. :param func: Function to call each time new task arguments become available. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function ``func`` :param iterable_len: Number of elements in the ``iterable_of_args``. When chunk_size is set to ``None`` it needs to know the number of tasks. This can either be provided by implementing the ``__len__`` function on the iterable object, or by specifying the number of tasks :param max_tasks_active: Maximum number of active tasks in the queue. If ``None`` it will be converted to ``n_jobs * chunk_size * 2`` :param chunk_size: Number of simultaneous tasks to give to a worker. When ``None`` it will use ``n_splits``. :param n_splits: Number of splits to use when ``chunk_size`` is ``None``. When both ``chunk_size`` and ``n_splits`` are ``None``, it will use ``n_splits = n_jobs * 64``. :param worker_lifespan: Number of tasks a worker can handle before it is restarted. If ``None``, workers will stay alive the entire time. Use this when workers use up too much memory over the course of time :param progress_bar: When ``True`` it will display a progress bar :param concatenate_numpy_output: When ``True`` it will concatenate numpy output to a single numpy array :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param progress_bar_style: The progress bar style to use. Can be one of ``None``, ``'std'``, or ``'notebook'`` :return: List with ordered results """ # Notify workers to keep order in mind self._worker_comms.signal_keep_order() # If we're dealing with numpy arrays, we have to chunk them here already if NUMPY_INSTALLED and isinstance(iterable_of_args, np.ndarray): iterable_of_args, iterable_len, chunk_size, n_splits = apply_numpy_chunking(iterable_of_args, iterable_len, chunk_size, n_splits, self.pool_params.n_jobs) # Process all args if iterable_len is None and hasattr(iterable_of_args, '__len__'): iterable_len = len(iterable_of_args) results = self.map_unordered( func, ((args_idx, args) for args_idx, args in enumerate(iterable_of_args)), iterable_len, max_tasks_active, chunk_size, n_splits, worker_lifespan, progress_bar, worker_init, worker_exit, task_timeout, worker_init_timeout, worker_exit_timeout, progress_bar_options, progress_bar_style ) # Notify workers to forget about order self._worker_comms.clear_keep_order() # Rearrange and return sorted_results = [result[1] for result in sorted(results, key=lambda result: result[0])] # Convert back to numpy if necessary return (np.concatenate(sorted_results) if NUMPY_INSTALLED and sorted_results and concatenate_numpy_output and isinstance(sorted_results[0], np.ndarray) else sorted_results) def map_unordered(self, func: Callable, iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int] = None, max_tasks_active: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, worker_lifespan: Optional[int] = None, progress_bar: bool = False, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None, progress_bar_options: Optional[Dict[str, Any]] = None, progress_bar_style: Optional[str] = None) -> Any: """ Same as ``multiprocessing.map()``, but unordered. Also allows a user to set the maximum number of tasks available in the queue. :param func: Function to call each time new task arguments become available. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function ``func`` :param iterable_len: Number of elements in the ``iterable_of_args``. When chunk_size is set to ``None`` it needs to know the number of tasks. This can either be provided by implementing the ``__len__`` function on the iterable object, or by specifying the number of tasks :param max_tasks_active: Maximum number of active tasks in the queue. If ``None`` it will be converted to ``n_jobs * chunk_size * 2`` :param chunk_size: Number of simultaneous tasks to give to a worker. When ``None`` it will use ``n_splits``. :param n_splits: Number of splits to use when ``chunk_size`` is ``None``. When both ``chunk_size`` and ``n_splits`` are ``None``, it will use ``n_splits = n_jobs * 64``. :param worker_lifespan: Number of tasks a worker can handle before it is restarted. If ``None``, workers will stay alive the entire time. Use this when workers use up too much memory over the course of time :param progress_bar: When ``True`` it will display a progress bar :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param progress_bar_style: The progress bar style to use. Can be one of ``None``, ``'std'``, or ``'notebook'`` :return: List with unordered results """ # Simply call imap and cast it to a list. This make sure all elements are there before returning return list(self.imap_unordered(func, iterable_of_args, iterable_len, max_tasks_active, chunk_size, n_splits, worker_lifespan, progress_bar, worker_init, worker_exit, task_timeout, worker_init_timeout, worker_exit_timeout, progress_bar_options, progress_bar_style)) def imap(self, func: Callable, iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int] = None, max_tasks_active: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, worker_lifespan: Optional[int] = None, progress_bar: bool = False, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None, progress_bar_options: Optional[Dict[str, Any]] = None, progress_bar_style: Optional[str] = None) -> Generator[Any, None, None]: """ Same as ``multiprocessing.imap_unordered()``, but ordered. Also allows a user to set the maximum number of tasks available in the queue. :param func: Function to call each time new task arguments become available. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function ``func`` :param iterable_len: Number of elements in the ``iterable_of_args``. When chunk_size is set to ``None`` it needs to know the number of tasks. This can either be provided by implementing the ``__len__`` function on the iterable object, or by specifying the number of tasks :param max_tasks_active: Maximum number of active tasks in the queue. If ``None`` it will be converted to ``n_jobs * chunk_size * 2`` :param chunk_size: Number of simultaneous tasks to give to a worker. When ``None`` it will use ``n_splits``. :param n_splits: Number of splits to use when ``chunk_size`` is ``None``. When both ``chunk_size`` and ``n_splits`` are ``None``, it will use ``n_splits = n_jobs * 64``. :param worker_lifespan: Number of tasks a worker can handle before it is restarted. If ``None``, workers will stay alive the entire time. Use this when workers use up too much memory over the course of time :param progress_bar: When ``True`` it will display a progress bar :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param progress_bar_style: The progress bar style to use. Can be one of ``None``, ``'std'``, or ``'notebook'`` :return: Generator yielding ordered results """ # Notify workers to keep order in mind self._worker_comms.signal_keep_order() # If we're dealing with numpy arrays, we have to chunk them here already if NUMPY_INSTALLED and isinstance(iterable_of_args, np.ndarray): iterable_of_args, iterable_len, chunk_size, n_splits = apply_numpy_chunking(iterable_of_args, iterable_len, chunk_size, n_splits, self.pool_params.n_jobs) # Yield results in order next_result_idx = 0 tmp_results = {} if iterable_len is None and hasattr(iterable_of_args, '__len__'): iterable_len = len(iterable_of_args) for result_idx, result in self.imap_unordered(func, ((args_idx, args) for args_idx, args in enumerate(iterable_of_args)), iterable_len, max_tasks_active, chunk_size, n_splits, worker_lifespan, progress_bar, worker_init, worker_exit, task_timeout, worker_init_timeout, worker_exit_timeout, progress_bar_options, progress_bar_style): # Check if the next one(s) to return is/are temporarily stored. We use a while-true block with dict.pop() to # keep the temporary store as small as possible while True: if next_result_idx in tmp_results: yield tmp_results.pop(next_result_idx) next_result_idx += 1 else: break # Check if the current result is the next one to return. If so, return it if result_idx == next_result_idx: yield result next_result_idx += 1 # Otherwise, temporarily store the current result else: tmp_results[result_idx] = result # Yield all remaining results for result_idx in sorted(tmp_results.keys()): yield tmp_results.pop(result_idx) # Notify workers to forget about order self._worker_comms.clear_keep_order() def imap_unordered(self, func: Callable, iterable_of_args: Union[Sized, Iterable], iterable_len: Optional[int] = None, max_tasks_active: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, worker_lifespan: Optional[int] = None, progress_bar: bool = False, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None, progress_bar_options: Optional[Dict[str, Any]] = None, progress_bar_style: Optional[str] = None) -> Generator[Any, None, None]: """ Same as ``multiprocessing.imap_unordered()``. Also allows a user to set the maximum number of tasks available in the queue. :param func: Function to call each time new task arguments become available. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function ``func`` :param iterable_len: Number of elements in the ``iterable_of_args``. When chunk_size is set to ``None`` it needs to know the number of tasks. This can either be provided by implementing the ``__len__`` function on the iterable object, or by specifying the number of tasks :param max_tasks_active: Maximum number of active tasks in the queue. If ``None`` it will be converted to ``n_jobs * chunk_size * 2`` :param chunk_size: Number of simultaneous tasks to give to a worker. When ``None`` it will use ``n_splits``. :param n_splits: Number of splits to use when ``chunk_size`` is ``None``. When both ``chunk_size`` and ``n_splits`` are ``None``, it will use ``n_splits = n_jobs * 64``. :param worker_lifespan: Number of tasks a worker can handle before it is restarted. If ``None``, workers will stay alive the entire time. Use this when workers use up too much memory over the course of time :param progress_bar: When ``True`` it will display a progress bar :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. The arguments ``total`` and ``leave`` will be overwritten by MPIRE. :param progress_bar_style: The progress bar style to use. Can be one of ``None``, ``'std'``, or ``'notebook'`` :return: Generator yielding unordered results """ # If we're dealing with numpy arrays, we have to chunk them here already iterator_of_chunked_args = [] numpy_chunking = False if NUMPY_INSTALLED and isinstance(iterable_of_args, np.ndarray): iterator_of_chunked_args, iterable_len, chunk_size, n_splits = apply_numpy_chunking( iterable_of_args, iterable_len, chunk_size, n_splits, self.pool_params.n_jobs ) numpy_chunking = True # Check parameters and thereby obtain the number of tasks. The chunk_size and progress bar parameters could be # modified as well n_tasks, max_tasks_active, chunk_size, progress_bar, progress_bar_options = check_map_parameters( self.pool_params, iterable_of_args, iterable_len, max_tasks_active, chunk_size, n_splits, worker_lifespan, progress_bar, progress_bar_options, progress_bar_style, task_timeout, worker_init_timeout, worker_exit_timeout ) new_map_params = WorkerMapParams(func, worker_init, worker_exit, worker_lifespan, progress_bar, task_timeout, worker_init_timeout, worker_exit_timeout) # Chunk the function arguments. Make single arguments when we're not dealing with numpy arrays if not numpy_chunking: iterator_of_chunked_args = chunk_tasks(iterable_of_args, n_tasks, chunk_size, n_splits) # Grab original lock in case we have a progress bar and we need to restore it tqdm = get_tqdm(progress_bar_style) original_tqdm_lock = tqdm.get_lock() tqdm_manager_owner = False imap_iterator = None try: if self._map_running: self._worker_comms.signal_exception_thrown(MAIN_PROCESS) self._cache[MAIN_PROCESS]._set(success=False, result=RuntimeError("Cannot call 'map' while another 'map' is running")) self._handle_exception() self._map_running = True # Start tqdm manager if a progress bar is desired. Will only start one when not already started. This has to # be done before starting the workers in case nested pools are used if progress_bar: tqdm_manager_owner = TqdmManager.start_manager(self.pool_params.use_dill) # Start workers if there aren't any. If they already exist check if we need to pass on new parameters if self._workers and not self._worker_comms.is_initialized(): logger.warning("WorkerPool parameters changed while keep_alive=True. Restarting workers.") self.stop_and_join(keep_alive=False) if self._workers and (self.map_params != new_map_params): self.map_params = new_map_params self._worker_comms.add_new_map_params(new_map_params) if not self._workers: self.map_params = new_map_params self._start_workers() # Create async result objects. The imap_iterator container will be used to store the results from the # workers. We can yield from that imap_iterator = UnorderedAsyncResultIterator(self._cache, n_tasks, timeout=task_timeout) job_id = imap_iterator.job_id # Create progress bar handler, which receives progress updates from the workers and updates the progress bar # accordingly with ProgressBarHandler(self.pool_params, self.map_params, progress_bar, progress_bar_options, progress_bar_style, self._worker_comms, self._worker_insights) as self._progress_bar_handler: try: # Process all args in the iterable n_active = 0 n_tasks = 0 while True: # Obtain next chunk of tasks try: chunk_of_tasks = next(iterator_of_chunked_args) n_tasks += len(chunk_of_tasks) except StopIteration: break # To keep the number of active tasks below max_tasks_active, we have to wait for results while (not self._worker_comms.exception_thrown() and n_active + len(chunk_of_tasks) > max_tasks_active): try: yield imap_iterator.next(block=True, timeout=0.01) n_active -= 1 except queue.Empty: pass # If an exception has been thrown, stop now if self._worker_comms.exception_thrown(): break self._worker_comms.add_task(job_id, chunk_of_tasks) n_active += len(chunk_of_tasks) # Obtain the results not yet obtained if not self._worker_comms.exception_thrown(): imap_iterator.set_length(n_tasks) self._progress_bar_handler.set_new_total(n_tasks) while not self._worker_comms.exception_thrown(): try: yield imap_iterator.next(block=True, timeout=0.1) except queue.Empty: pass except StopIteration: break # Terminate if exception has been thrown at this point if self._worker_comms.exception_thrown(): self._handle_exception() # All results are in: it's clean up time self.stop_and_join(keep_alive=self.pool_params.keep_alive) # Wait for the progress bar to finish, before we clean it up if progress_bar: self._worker_comms.wait_until_progress_bar_is_complete() except KeyboardInterrupt: self._handle_exception() finally: if tqdm_manager_owner: tqdm.set_lock(original_tqdm_lock) TqdmManager.stop_manager() if imap_iterator is not None: imap_iterator.remove_from_cache() self._progress_bar_handler = None self._map_running = False self._worker_comms.reset_progress() # Log insights if self.pool_params.enable_insights: logger.debug(self._worker_insights.get_insights_string()) def apply(self, func: Callable, args: Any = (), kwargs: Dict = None, callback: Optional[Callable] = None, error_callback: Optional[Callable] = None, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None) -> Any: """ Apply a function to a single task. This is a blocking call. :param func: Function to apply to the task. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param args: Arguments to pass to a worker, which passes it to the function ``func`` as ``func(*args)`` :param kwargs: Keyword arguments to pass to a worker, which passes it to the function ``func`` as ``func(**kwargs)`` :param callback: Callback function to call when the task is finished. The callback function receives the output of the function ``func`` as its argument :param error_callback: Callback function to call when the task has failed. The callback function receives the exception as its argument :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :return: Result of the function ``func`` applied to the task """ return self.apply_async(func, args, kwargs, callback, error_callback, worker_init, worker_exit, task_timeout, worker_init_timeout, worker_exit_timeout).get() def apply_async(self, func: Callable, args: Any = (), kwargs: Dict = None, callback: Optional[Callable] = None, error_callback: Optional[Callable] = None, worker_init: Optional[Callable] = None, worker_exit: Optional[Callable] = None, task_timeout: Optional[float] = None, worker_init_timeout: Optional[float] = None, worker_exit_timeout: Optional[float] = None) -> AsyncResult: """ Apply a function to a single task. This is a non-blocking call. :param func: Function to apply to the task. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param args: Arguments to pass to a worker, which passes it to the function ``func`` as ``func(*args)`` :param kwargs: Keyword arguments to pass to a worker, which passes it to the function ``func`` as ``func(**kwargs)`` :param callback: Callback function to call when the task is finished. The callback function receives the output of the function ``func`` as its argument :param error_callback: Callback function to call when the task has failed. The callback function receives the exception as its argument :param worker_init: Function to call each time a new worker starts. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param worker_exit: Function to call each time a worker exits. Return values will be fetched and made available through :obj:`mpire.WorkerPool.get_exit_results`. When passing on the worker ID the function should receive the worker ID as its first argument. If shared objects are provided the function should receive those as the next argument. If the worker state has been enabled it should receive a state variable as the next argument :param task_timeout: Timeout in seconds for a single task. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). Note: the timeout doesn't apply to ``worker_init`` and ``worker_exit`` functions, use `worker_init_timeout` and `worker_exit_timeout` for that, respectively :param worker_init_timeout: Timeout in seconds for the ``worker_init`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :param worker_exit_timeout: Timeout in seconds for the ``worker_exit`` function. When the timeout is exceeded, MPIRE will raise a ``TimeoutError``. Use ``None`` to disable (default). :return: Result of the function ``func`` applied to the task """ # Check if the pool has been started if not self._workers: self.map_params = WorkerMapParams(func, worker_init, worker_exit, None, False, task_timeout, worker_init_timeout, worker_exit_timeout) self._start_workers() # Add task to the queue result = AsyncResult(self._cache, callback, error_callback, timeout=task_timeout) self._worker_comms.add_apply_task(result.job_id, func, args, kwargs) return result def _handle_exception(self) -> None: """ Handles exceptions thrown by workers and KeyboardInterrupts """ # Obtain exception if self._worker_comms.exception_thrown(): exception = self._cache[self._worker_comms.get_exception_thrown_job_id()].get_exception() cause = exception.__cause__ else: self._worker_comms.signal_exception_thrown(MAIN_PROCESS) exception = KeyboardInterrupt() cause = exception # Pass error to progress bar, if there is one. We are interested in the cause of the exception, as that contains # the traceback from the worker. If there is no cause, we use the exception itself (e.g., a TimeoutError thrown # in the timeout handler) if self._progress_bar_handler is not None: self._progress_bar_handler.set_exception(cause or exception) if self._progress_bar_handler.thread is not None: self._progress_bar_handler.thread.join() self._progress_bar_handler = None self.terminate() # Clear keep order event so we can safely reuse the WorkerPool and use (i)map_unordered after an (i)map call self._worker_comms.clear_keep_order() # Raise raise exception def stop_and_join(self, keep_alive: bool = False) -> None: """ When ``keep_alive=False``: inserts a poison pill, grabs the exit results, waits until the tasks/results queues are done, and waits until all workers are finished. When ``keep_alive=True``: inserts a non-lethal poison pill, and waits until the tasks/results queues are done. ``join``and ``stop_and_join`` are aliases. :param keep_alive: Whether to keep the workers alive """ if self._workers: # All tasks have been processed and results are in. Insert (non-lethal) poison pill if keep_alive: self._worker_comms.insert_non_lethal_poison_pill() else: self._worker_comms.insert_poison_pill() # Wait until all (non-lethal) poison pills have been consumed. When a worker's lifetime has been reached # just before consuming the poison pill, we need to restart them t = threading.Thread(target=self._worker_comms.join_task_queues, args=(keep_alive,)) t.daemon = True t.start() while not self._worker_comms.exception_thrown(): t.join(timeout=0.01) if not t.is_alive(): break # When an exception occurred in the above process (i.e., the worker init function raises), we need to # handle the exception (i.e., terminate and raise) if self._worker_comms.exception_thrown(): self._handle_exception() # Join workers if not keep_alive: for wid, worker_process in enumerate(self._workers): try: worker_process.join() except ValueError: raise # Added since Python 3.7. This will clean up any resources that are left. For some reason though, # when using daemon processes and nested pools, a process can still be alive after the join when # close is called and a ValueError is raised. So we wait a bit and check if the process will die. If # not, then the GC can clean up the resources later. if hasattr(worker_process, 'close'): try_count = 5 while worker_process.is_alive() and try_count > 0: time.sleep(0.01) try_count -= 1 try: worker_process.close() except ValueError: pass self._workers = [] # Join the results queue, but do not close it. All results should be in the cache at this point (including # exit results, because the workers joined successfully or keep_alive=True and the exit function isn't # called), but we still need this queue for closing the results listener thread self._worker_comms.join_results_queues(keep_alive=True) # If an exception occurred in the exit function, we need to handle the exception (i.e., terminate and raise) if self._worker_comms.exception_thrown(): self._handle_exception() # Stop handler threads and join and close the results queue if we're not keeping the workers alive if not keep_alive: self._stop_handler_threads() self._worker_comms.join_results_queues(keep_alive=False) join = stop_and_join def terminate(self) -> None: """ Tries to do a graceful shutdown of the workers, by interrupting them. In the case processes deadlock it will send a sigkill. """ if not self._workers: return # Set exception thrown so workers know to stop fetching new tasks if not self._worker_comms.exception_thrown(): self._worker_comms.signal_exception_thrown(MAIN_PROCESS) self._cache[MAIN_PROCESS]._set(success=False, result=RuntimeError("Pool was terminated")) # If this function is called from handle_exception, the progress bar is already terminated. If not, we need to # terminate it here if self._progress_bar_handler is not None: self._progress_bar_handler.set_exception(RuntimeError("Pool was terminated")) # When we're working with threads we have to wait for them to join. We can't kill threads in Python if self.pool_params.start_method == 'threading': threads = self._workers else: # Create cleanup threads such that processes can get killed simultaneously, which can save quite some time threads = [] dont_wait_event = threading.Event() dont_wait_event.set() for worker_id in range(self.pool_params.n_jobs): t = threading.Thread(target=self._terminate_worker, args=(worker_id, dont_wait_event)) t.start() threads.append(t) # Wait until cleanup threads are done for t in threads: t.join() # Stop handler threads self._stop_handler_threads() # Drain and join the queues self._worker_comms.drain_queues() # Reset workers and cache. Keep only the main process, init and exit results objects self._workers = [] self._cache = {key: self._cache[key] for key in (MAIN_PROCESS, INIT_FUNC, EXIT_FUNC)} def _terminate_worker(self, worker_id: int, dont_wait_event: threading.Event) -> None: """ Terminates a single worker process. When a process.join() raises an AssertionError, it means the worker hasn't started yet. In that case, we simply return. A ValueError can be raised on Windows systems. :param worker_id: Worker ID :param dont_wait_event: Event object to indicate whether other termination threads should continue. I.e., when we set it to False, threads should wait. """ # When a worker didn't start in the first place, we don't have to do anything if self._workers[worker_id] is None or self._workers[worker_id].pid is None: return # Send a kill signal to the worker self._send_kill_signal_to_worker(worker_id) # We wait until workers are done terminating. However, we don't have all the patience in the world. When the # patience runs out we terminate them. try_count = 10 while try_count > 0: try: self._workers[worker_id].join(timeout=0.1) if not self._workers[worker_id].is_alive(): break except AssertionError: return except ValueError: pass # For properly joining, it can help if we try to get some results here. Workers can still be busy putting # items in queues under the hood, even at this point. self._worker_comms.drain_results_queue_terminate_worker(dont_wait_event) try_count -= 1 if not dont_wait_event.is_set(): dont_wait_event.wait() # If, after all this, the worker is still alive, we terminate it with a brutal kill signal. This shouldn't # really happen. But, better safe than sorry try: if self._workers[worker_id].is_alive(): self._workers[worker_id].terminate() self._workers[worker_id].join() except AssertionError: return except ValueError: pass # Added since Python 3.7 if hasattr(self._workers[worker_id], 'close'): try: self._workers[worker_id].close() except AssertionError: return except ValueError: pass def _send_kill_signal_to_worker(self, worker_id: int) -> None: """ Sends a kill signal to a worker process, but only if we know it's running a task. :param worker_id: Worker ID """ # Signal handling in Windows is cumbersome, to say the least. Therefore, it handles error handling # differently. See Worker::_exit_gracefully_windows for more information. if not RUNNING_WINDOWS and self.pool_params.start_method != "threading": with self._worker_comms.get_worker_running_task_lock(worker_id): if self._worker_comms.get_worker_running_task(worker_id): # A signal should only be send once self._worker_comms.set_worker_running_task(worker_id, False) # Send signal try: os.kill(self._workers[worker_id].pid, signal.SIGUSR1) except (ProcessLookupError, ValueError): pass def _stop_handler_threads(self) -> None: """ Stops results, restart, timeout, and unexpected death handler threads. """ self._handler_threads_stop_event.set() # Join results listener thread if self._results_handler_thread is not None and self._results_handler_thread.is_alive(): self._worker_comms.insert_poison_pill_results_listener() self._results_handler_thread.join() self._results_handler_thread = None # Join restart thread if self._restart_handler_thread is not None and self._restart_handler_thread.is_alive(): self._worker_comms.signal_worker_restart_condition() self._restart_handler_thread.join() self._restart_handler_thread = None # Join timeout handler thread if self._timeout_handler_thread is not None and self._timeout_handler_thread.is_alive(): self._timeout_handler_thread.join() self._timeout_handler_thread = None # Join unexpected death handler thread if (self._unexpected_death_handler_thread is not None and self._unexpected_death_handler_thread.is_alive()): self._unexpected_death_handler_thread.join() self._unexpected_death_handler_thread = None def print_insights(self) -> None: """ Prints insights per worker """ print(self._worker_insights.get_insights_string()) def get_insights(self) -> Dict: """ Creates insights from the raw insight data :return: Dictionary containing worker insights """ return self._worker_insights.get_insights() mpire-2.10.2/mpire/progress_bar.py000066400000000000000000000304641461637447300171140ustar00rootroot00000000000000import threading import traceback from datetime import datetime, timedelta from threading import Event, Thread from typing import Any, Dict, Optional, Type import warnings from tqdm import TqdmExperimentalWarning, tqdm as tqdm_type from mpire.comms import WorkerComms, POISON_PILL from mpire.exception import remove_highlighting from mpire.insights import WorkerInsights from mpire.params import WorkerMapParams, WorkerPoolParams from mpire.signal import DisableKeyboardInterruptSignal from mpire.tqdm_utils import get_tqdm, TqdmManager from mpire.utils import format_seconds # If a user has not installed the dashboard dependencies than the imports below will fail try: from mpire.dashboard.dashboard import DASHBOARD_STARTED_EVENT from mpire.dashboard.utils import get_function_details from mpire.dashboard.manager import get_manager_client_dicts except ImportError: DASHBOARD_STARTED_EVENT = None def get_function_details(_): pass def get_manager_client_dicts(): raise NotImplementedError DATETIME_FORMAT = "%Y-%m-%d, %H:%M:%S" class ProgressBarHandler: def __init__(self, pool_params: WorkerPoolParams, map_params: WorkerMapParams, show_progress_bar: bool, progress_bar_options: Dict[str, Any], progress_bar_style: Optional[str], worker_comms: WorkerComms, worker_insights: WorkerInsights) -> None: """ :param pool_params: WorkerPool parameters :param map_params: Map parameters :param show_progress_bar: When ``True`` will display a progress bar :param progress_bar_options: Dictionary containing keyword arguments to pass to the ``tqdm`` progress bar. See ``tqdm.tqdm()`` for details. :param progress_bar_style: The progress bar style to use :param worker_comms: Worker communication objects (queues, locks, events, ...) :param worker_insights: WorkerInsights object which stores the worker insights """ self.show_progress_bar = show_progress_bar self.progress_bar_options = progress_bar_options self.progress_bar_style = progress_bar_style self.worker_comms = worker_comms self.worker_insights = worker_insights if show_progress_bar and DASHBOARD_STARTED_EVENT is not None and DASHBOARD_STARTED_EVENT.is_set(): self.function_details = get_function_details(map_params.func) self.function_details['n_jobs'] = pool_params.n_jobs else: self.function_details = None self.thread = None self.thread_started = Event() self.progress_bar_id = None self.total = None self.total_updated = Event() self.exception_traceback_str = None self.exception_traceback_str_set_condition = threading.Condition(lock=threading.Lock()) self.dashboard_dict = None self.dashboard_details_dict = None self.start_t = None def __enter__(self) -> 'ProgressBarHandler': """ Enables the use of the ``with`` statement. Starts a new progress handler thread if a progress bar should be shown :return: self """ if self.show_progress_bar: # Disable the interrupt signal. We let the thread die gracefully with DisableKeyboardInterruptSignal(): self.thread = Thread(target=self._progress_bar_handler) self.thread.start() self.thread_started.wait() return self def __exit__(self, exc_type: Type, *_) -> None: """ Enables the use of the ``with`` statement. Terminates the progress handler thread if there is one """ if self.show_progress_bar and self.thread.is_alive(): # If this exit is called with an exception, then we assume an external kill signal was received (this is, # for example, necessary in nested pools when an error occurs) if exc_type is not None: self.worker_comms.signal_kill_signal_received() # Signal shutdown and close the handling thread if not self.worker_comms.exception_thrown(): self.worker_comms.signal_progress_bar_shutdown() self.thread.join() def _progress_bar_handler(self) -> None: """ Keeps track of the progress made by the workers and updates the progress bar accordingly """ # Obtain the progress bar tqdm class tqdm = get_tqdm(self.progress_bar_style) # Connect to the tqdm manager tqdm_manager = TqdmManager() tqdm_lock, tqdm_position_register = tqdm_manager.get_connection_details() tqdm.set_lock(tqdm_lock) tqdm.set_main_progress_bar( tqdm_position_register.register_progress_bar_position(self.progress_bar_options["position"]) ) # Create progress bar and register the start time. Ignore the experimental warning for rich progress bars with warnings.catch_warnings(): warnings.simplefilter("ignore", TqdmExperimentalWarning) tqdm.monitor_interval = False progress_bar = tqdm(**self.progress_bar_options) self.start_t = datetime.fromtimestamp(progress_bar.start_t) # Notify that the main process can continue working. We set it after the progress bar has been created, instead # of right after this thread has started, for a better user experience self.thread_started.set() # Register progress bar to dashboard in case a dashboard is started self._register_progress_bar(progress_bar) while True: # Wait for a job to finish tasks_completed = self.worker_comms.get_tasks_completed_progress_bar() # If we received a poison pill, we should quit right away. We do force a final refresh of the progress bar # to show the latest status if tasks_completed is POISON_PILL: # Check if we got a poison pill because there was an error. If so, we obtain the exception information # and send it to the dashboard, if available.) if self.worker_comms.exception_thrown() or self.worker_comms.kill_signal_received(): progress_bar.set_description('Exception occurred, terminating ... ') if self.worker_comms.exception_thrown(): # Wait for exception traceback str to be set with self.exception_traceback_str_set_condition: if self.exception_traceback_str is None: self.exception_traceback_str_set_condition.wait() self._send_dashboard_update(progress_bar, failed=True, traceback_str=self.exception_traceback_str) elif self.worker_comms.kill_signal_received(): self._send_dashboard_update(progress_bar, failed=True, traceback_str='Kill signal received') # Final update of the progress bar progress_bar.final_refresh(tqdm_position_register.get_highest_progress_bar_position()) break # Check if the total has been updated. It could be that we didn't know the total number of tasks at the # beginning, but we do now. if self.total_updated.is_set(): progress_bar.update_total(self.total) self._send_dashboard_update(progress_bar) self.total_updated.clear() # Check if there's an actual update if tasks_completed > 0 and tasks_completed == progress_bar.n: continue # Update progress bar progress_bar.update(tasks_completed - progress_bar.n) if progress_bar.n == progress_bar.total: self.worker_comms.signal_progress_bar_complete() self.worker_comms.wait_until_progress_bar_is_complete() self._send_dashboard_update(progress_bar) # Send update to dashboard in case a dashboard is started, but only when tqdm updated its view as well. This # will make the dashboard a lot more responsive if progress_bar.n == progress_bar.last_print_n: self._send_dashboard_update(progress_bar) def _register_progress_bar(self, progress_bar: tqdm_type) -> None: """ Register this progress bar to the dashboard :param progress_bar: tqdm progress bar instance """ if self.progress_bar_id is None and DASHBOARD_STARTED_EVENT is not None and DASHBOARD_STARTED_EVENT.is_set(): # Connect to manager server self.dashboard_dict, self.dashboard_details_dict, dashboard_tqdm_lock = get_manager_client_dicts() # Register new progress bar dashboard_tqdm_lock.acquire() self.progress_bar_id = len(self.dashboard_dict.keys()) + 1 self.dashboard_details_dict.update([(self.progress_bar_id, self.function_details)]) self._send_dashboard_update(progress_bar) dashboard_tqdm_lock.release() def _send_dashboard_update(self, progress_bar: tqdm_type, failed: bool = False, traceback_str: Optional[str] = None) -> None: """ Adds a progress bar update to the shared dict so the dashboard process can use it, only when a dashboard has started :param progress_bar: tqdm progress bar instance :param failed: Whether or not the operation failed or not :param traceback_str: Traceback string, if an exception was raised """ if self.progress_bar_id is not None: self.dashboard_dict.update([(self.progress_bar_id, self._get_progress_bar_update_dict(progress_bar, failed, traceback_str))]) def _get_progress_bar_update_dict(self, progress_bar: tqdm_type, failed: bool, traceback_str: Optional[str] = None) -> Dict[str, Any]: """ Obtain update dictionary with all the information needed for displaying on the dashboard :param progress_bar: tqdm progress bar instance :param failed: Whether or not the operation failed or not :param traceback_str: Traceback string, if an exception was raised :return: Update dictionary """ # Save some variables first so we can use them consistently with the same value details = progress_bar.format_dict n = details["n"] total = details["total"] now = datetime.now() rate = details["rate"] if details["rate"] else n / details["elapsed"] if details["elapsed"] else None remaining_time = (total - n) / rate if total and rate else None return {"id": self.progress_bar_id, "success": not failed, "n": n, "total": total, "percentage": n / total if total else None, "duration": str(now - self.start_t).rsplit('.', 1)[0], "remaining": format_seconds(remaining_time, False), "started_raw": self.start_t, "started": self.start_t.strftime(DATETIME_FORMAT), "finished_raw": now + timedelta(seconds=remaining_time) if remaining_time is not None else None, "finished": ((now + timedelta(seconds=remaining_time)).strftime(DATETIME_FORMAT) if remaining_time is not None else ''), "traceback": traceback_str.strip() if traceback_str is not None else None, "insights": self.worker_insights.get_insights()} def set_new_total(self, total: int) -> None: """ Set a new total for the progress bar :param total: New total """ self.total = total self.total_updated.set() def set_exception(self, traceback_err: Exception) -> None: """ Set the exception traceback string and notify the progress bar handler that it's ready :param traceback_err: Traceback error """ if traceback_err.__cause__ is not None: traceback_str = "".join(traceback.format_tb(traceback_err.__traceback__)) else: traceback_str = str(traceback_err) with self.exception_traceback_str_set_condition: self.exception_traceback_str = remove_highlighting(traceback_str) self.exception_traceback_str_set_condition.notify() mpire-2.10.2/mpire/py.typed000066400000000000000000000000001461637447300155300ustar00rootroot00000000000000mpire-2.10.2/mpire/signal.py000066400000000000000000000027011461637447300156720ustar00rootroot00000000000000from inspect import Traceback from signal import getsignal, SIG_IGN, SIGINT, signal as signal_, Signals from threading import current_thread, main_thread from types import FrameType from typing import Type class DelayedKeyboardInterrupt: def __init__(self) -> None: self.signal_received = None def __enter__(self) -> None: # When we're in a thread we can't use signal handling if current_thread() == main_thread(): self.signal_received = False self.old_handler = signal_(SIGINT, self.handler) def handler(self, sig: Signals, frame: FrameType) -> None: self.signal_received = (sig, frame) def __exit__(self, exc_type: Type, exc_val: Exception, exc_tb: Traceback) -> None: if current_thread() == main_thread(): signal_(SIGINT, self.old_handler) if self.signal_received: self.old_handler(*self.signal_received) class DisableKeyboardInterruptSignal: def __enter__(self) -> None: if current_thread() == main_thread(): # Prevent signal from propagating to child process self._handler = getsignal(SIGINT) ignore_keyboard_interrupt() def __exit__(self, exc_type: Type, exc_val: Exception, exc_tb: Traceback) -> None: if current_thread() == main_thread(): # Restore signal signal_(SIGINT, self._handler) def ignore_keyboard_interrupt(): signal_(SIGINT, SIG_IGN) mpire-2.10.2/mpire/tqdm_utils.py000066400000000000000000000260201461637447300166020ustar00rootroot00000000000000import logging import warnings from contextlib import redirect_stderr, redirect_stdout from io import StringIO from multiprocessing import Lock as mp_Lock from multiprocessing.synchronize import Lock as LockType from typing import Optional, Tuple, Type from tqdm import TqdmExperimentalWarning, tqdm as tqdm_std from tqdm.notebook import tqdm as tqdm_notebook try: from tqdm.rich import tqdm as tqdm_rich RICH_AVAILABLE = True except ImportError: tqdm_rich = None RICH_AVAILABLE = False from mpire.context import mp_dill from mpire.signal import DisableKeyboardInterruptSignal from mpire.utils import create_sync_manager PROGRESS_BAR_DEFAULT_STYLE = 'std' TqdmConnectionDetails = Tuple[LockType, "TqdmPositionRegister"] logger = logging.getLogger(__name__) class TqdmMpire: """ Abstract class for tqdm classes that are used in mpire""" main_progress_bar = False @classmethod def set_main_progress_bar(cls, main: bool) -> None: """ Marks this progress bar as the main progress bar :param main: Whether this progress bar is the main progress bar """ cls.main_progress_bar = main def update(self, n: int = 1) -> None: """ Update the progress bar. Forces a final refresh when the progress bar is finished. :param n: Number of steps to update the progress bar with """ super().update(n) if self.n == self.total: self.final_refresh() def update_total(self, total: int) -> None: """ Update the total number of steps of the progress bar. Forces a refresh to show the new total. :param total: Total number of steps """ self.total = total self.refresh() def final_refresh(self, highest_progress_bar_position: Optional[int] = None) -> None: """ Final refresh of the progress bar. This function is called when the progress bar is finished. It should perform a final refresh of the progress bar and close it. :param highest_progress_bar_position: Highest progress bar position in case of multiple progress bars """ self.refresh() self.close() @classmethod def check_options(cls, options: dict) -> None: """ Check whether the options passed to the tqdm class are valid. This function should raise an exception when the options are invalid. :param options: Options passed to the tqdm class """ with redirect_stderr(StringIO()), redirect_stdout(StringIO()): cls(**options) class TqdmMpireStd(TqdmMpire, tqdm_std): """ A tqdm class that shows a standard progress bar. """ def final_refresh(self, highest_progress_bar_position: Optional[int] = None) -> None: """ Final refresh of the progress bar. This function is called when the progress bar is finished. It should perform a final refresh. When we're using a standard progress bar and this is the main progress bar, we add as many newlines as the highest progress bar position, such that new output is added after the progress bars. :param highest_progress_bar_position: Highest progress bar position in case of multiple progress bars """ self.refresh() self.disable = True if self.main_progress_bar and highest_progress_bar_position is not None: self.fp.write('\n' * (highest_progress_bar_position + 1)) if RICH_AVAILABLE: class TqdmMpireRich(TqdmMpire, tqdm_rich): """ A tqdm class that shows a rich progress bar. """ @classmethod def check_options(cls, options: dict) -> None: """ Check whether the options passed to the tqdm class are valid. This function should raise an exception when the options are invalid. For rich progress bars we disable the progress bar, because we don't want to show the progress bar in the terminal. For some reason, redirecting stdout/stderr makes the rich progress bar not work properly afterwards. :param options: Options passed to the tqdm class """ options = options.copy() if "options" not in options: options["options"] = {"disable": True} else: options["options"]["disable"] = True with warnings.catch_warnings(): warnings.simplefilter("ignore", TqdmExperimentalWarning) cls(**options) def display(self, *args, **kwargs) -> None: """ Display the progress bar and force a refresh of the widget. The refresh is needed to show the final update. """ super().display(*args, **kwargs) self._prog.refresh() else: class TqdmMpireRich(TqdmMpire): def __init__(self, *args, **kwargs) -> None: raise ImportError("rich is not installed. Please install rich to use rich progress bars.") class TqdmMpireNotebook(TqdmMpire, tqdm_notebook): """ A tqdm class that shows a GUI widget in notebooks. """ def __init__(self, *args, **kwargs) -> None: """ In case we're running tqdm in a notebook we need to apply a dirty hack to get progress bars working. Solution adapted from https://github.com/tqdm/tqdm/issues/485#issuecomment-473338308 """ if not self.main_progress_bar: print(' ', end='', flush=True) super().__init__(*args, **kwargs) def update_total(self, total: int) -> None: """ Update the total number of steps of the progress bar. Forces a refresh to show the new total. In a notebook we also need to update the max value of the progress bar widget. :param total: Total number of steps """ self.container.children[1].max = total return super().update_total(total) @classmethod def check_options(cls, options: dict) -> None: """ Check whether the options passed to the tqdm class are valid. This function should raise an exception when the options are invalid. For notebook progress bars we set display to false, because redirecting stdout/stderr doesn't work for notebook widgets. :param options: Options passed to the tqdm class """ options = options.copy() options["display"] = False cls(**options) class TqdmMpireDashboardOnly(TqdmMpire, tqdm_std): """ A tqdm class that gives no output, but will still update the internal progress-bar attributes that the dashboard relies on. """ def __init__(self, *args, **kwargs) -> None: """ Set the file to a StringIO object so that no output is given """ kwargs["file"] = StringIO() super().__init__(*args, **kwargs) def display(self, *args, **kwargs) -> None: """ Don't display anything """ pass def get_tqdm(progress_bar_style: Optional[str]) -> Type[TqdmMpire]: """ Get the tqdm class to use based on the progress bar style :param progress_bar_style: The progress bar style to use. Can be one of ``None``, ``std``, or ``notebook`` :return: A tuple containing the tqdm class to use and a boolean indicating whether the progress bar is a notebook widget """ if progress_bar_style is None: progress_bar_style = PROGRESS_BAR_DEFAULT_STYLE if progress_bar_style == 'std': return TqdmMpireStd elif progress_bar_style == 'rich': return TqdmMpireRich elif progress_bar_style == 'notebook': return TqdmMpireNotebook elif progress_bar_style == 'dashboard': return TqdmMpireDashboardOnly else: raise ValueError(f'Invalid progress bar style: {progress_bar_style}. ' f'Use either None (=default), "std", or "notebook"') class TqdmPositionRegister: """ Class that keeps track of all the registered progress bar positions. Needed to properly display multiple tqdm progress bars """ def __init__(self, use_dill: bool) -> None: """ :param use_dill: Whether dill is used as serialization library """ self.lock = mp_dill.Lock() if use_dill else mp_Lock() self.highest_position = None def register_progress_bar_position(self, position: int) -> bool: """ Register new progress bar position. Returns True when it's the first one to register :param position: Progress bar position :return: Whether this progress bar is the first one to register """ with self.lock: first_one = self.highest_position is None if self.highest_position is None or position > self.highest_position: self.highest_position = position return first_one def get_highest_progress_bar_position(self) -> Optional[int]: """ Obtain the highest registered progress bar position :return: Highest progress bar position """ with self.lock: return self.highest_position def reset_progress_bar_positions(self) -> None: """ Reset the registered progress bar positions """ with self.lock: self.highest_position = None class TqdmManager: """Tqdm manager wrapper for syncing multiple progress bars, independent of process start method used.""" MANAGER = None LOCK = None POSITION_REGISTER = None @classmethod def start_manager(cls, use_dill: bool) -> bool: """ Sets up and starts the tqdm manager :param use_dill: Whether dill is used as serialization library :return: Whether the manager was started """ # Don't do anything when there's already a tqdm manager that has started if cls.LOCK is not None: return False logger.debug("Starting TQDM manager") # Create manager with DisableKeyboardInterruptSignal(): cls.MANAGER = create_sync_manager(use_dill) cls.MANAGER.register('TqdmPositionRegister', TqdmPositionRegister) cls.MANAGER.start() cls.LOCK = cls.MANAGER.Lock() cls.POSITION_REGISTER = cls.MANAGER.TqdmPositionRegister(use_dill) return True @classmethod def stop_manager(cls) -> None: """ Stops the tqdm manager """ cls.MANAGER.shutdown() cls.MANAGER = None cls.LOCK = None cls.POSITION_REGISTER = None @classmethod def get_connection_details(cls) -> TqdmConnectionDetails: """ Obtains the connection details of the tqdm manager. These details are needed to be passed on to child process when the start method is either forkserver or spawn. :return: TQDM lock and position register """ return cls.LOCK, cls.POSITION_REGISTER @classmethod def set_connection_details(cls, tqdm_connection_details: TqdmConnectionDetails) -> None: """ Sets the tqdm connection details. :param tqdm_connection_details: TQDM lock and position register """ cls.LOCK, cls.POSITION_REGISTER = tqdm_connection_details mpire-2.10.2/mpire/utils.py000066400000000000000000000275541461637447300155720ustar00rootroot00000000000000import heapq import itertools import math import os import time from datetime import timedelta from multiprocessing import cpu_count from multiprocessing.managers import SyncManager from multiprocessing.sharedctypes import SynchronizedArray from typing import Callable, Collection, Generator, Iterable, List, Optional, Tuple, Union try: import numpy as np NUMPY_INSTALLED = True except ImportError: np = None NUMPY_INSTALLED = False from mpire.context import RUNNING_MACOS, RUNNING_WINDOWS, mp_dill # Needed for setting CPU affinity if RUNNING_WINDOWS: try: import win32api import win32con import win32process WIN32API_AVAILABLE = True WIN32API_ERROR = None except ImportError as e: WIN32API_AVAILABLE = False WIN32API_ERROR = e WIN32API_ERROR.msg += " If you're using Conda, you can run `conda install pywin32` to install the missing " \ "module." def set_cpu_affinity(pid: int, mask: List[int]) -> None: """ Sets the CPU affinity for a given process. On Windows-based systems with more than 64 processors, I'm not sure if this will work. See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setprocessaffinitymask#parameters. :param pid: Process ID :param mask: List of CPU IDs """ if RUNNING_WINDOWS: # On Conda systems simply install pywin32 doesn't always work. In those cases, you need to run # `conda install pywin32`. if not WIN32API_AVAILABLE: raise WIN32API_ERROR # Convert mask to something Windows understands windows_mask = 0 for cpu_id in mask: windows_mask ^= 2 ** cpu_id # Get handle and set affinity handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid) win32process.SetProcessAffinityMask(handle, windows_mask) elif RUNNING_MACOS: # On MacOS we can't set CPU affinity pass else: os.sched_setaffinity(pid, mask) def chunk_tasks(iterable_of_args: Iterable, iterable_len: Optional[int] = None, chunk_size: Optional[Union[int, float]] = None, n_splits: Optional[int] = None) \ -> Generator[Collection, None, None]: """ Chunks tasks such that individual workers will receive chunks of tasks rather than individual ones, which can speed up processing drastically. :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function :param iterable_len: Number of tasks available in ``iterable_of_args``. Only needed when ``iterable_of_args`` is a generator :param chunk_size: Number of simultaneous tasks to give to a worker. If ``None``, will use ``n_splits`` to determine the chunk size :param n_splits: Number of splits to use when ``chunk_size`` is ``None`` :return: Generator of chunked task arguments """ if chunk_size is None and n_splits is None: raise ValueError("chunk_size and n_splits cannot both be None") # Determine chunk size if chunk_size is None: # Get number of tasks if iterable_len is not None: n_tasks = iterable_len elif hasattr(iterable_of_args, '__len__'): n_tasks = len(iterable_of_args) else: raise ValueError('Either iterable_len or an iterable with a len() function should be provided when ' 'chunk_size and n_splits are None') # Determine chunk size chunk_size = n_tasks / n_splits # Chunk tasks args_iter = iter(iterable_of_args) current_chunk_size = chunk_size n_elements_returned = 0 while True: # Use numpy slicing if available. We use max(1, ...) to always at least get one element if NUMPY_INSTALLED and isinstance(iterable_of_args, np.ndarray): chunk = iterable_of_args[n_elements_returned:n_elements_returned + max(1, math.ceil(current_chunk_size))] else: chunk = tuple(itertools.islice(args_iter, max(1, math.ceil(current_chunk_size)))) # If we ran out of input, we stop if len(chunk) == 0: return # If the iterable has more elements than the given iterable length, we stop if iterable_len is not None and n_elements_returned + len(chunk) > iterable_len: chunk = chunk[:iterable_len - n_elements_returned] if chunk: yield chunk return yield chunk current_chunk_size = (current_chunk_size + chunk_size) - math.ceil(current_chunk_size) n_elements_returned += len(chunk) def apply_numpy_chunking(iterable_of_args: Iterable, iterable_len: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, n_jobs: Optional[int] = None) -> Tuple[Iterable, int, int, None]: """ If we're dealing with numpy arrays, chunk them using numpy slicing and return changed map parameters :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function :param iterable_len: When chunk_size is set to ``None`` it needs to know the number of tasks. This can either be provided by implementing the ``__len__`` function on the iterable object, or by specifying the number of tasks :param chunk_size: Number of simultaneous tasks to give to a worker. If ``None``, will generate ``n_jobs * 4`` number of chunks :param n_splits: Number of splits to use when ``chunk_size`` is ``None`` :param n_jobs: Number of workers to spawn. If ``None``, will use ``cpu_count()``. :return: Chunked ``iterable_of_args`` with updated ``iterable_len``, ``chunk_size`` and ``n_splits`` """ if iterable_len is not None: iterable_of_args = iterable_of_args[:iterable_len] iterable_len = get_n_chunks(iterable_of_args, iterable_len, chunk_size, n_splits, n_jobs) iterable_of_args = make_single_arguments(chunk_tasks(iterable_of_args, len(iterable_of_args), chunk_size, n_splits or (n_jobs * 4 if n_jobs is not None else None))) chunk_size = 1 n_splits = None return iterable_of_args, iterable_len, chunk_size, n_splits def get_n_chunks(iterable_of_args: Iterable, iterable_len: Optional[int] = None, chunk_size: Optional[int] = None, n_splits: Optional[int] = None, n_jobs: Optional[int] = None) -> int: """ Get number of chunks :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function :param iterable_len: Number of tasks available in ``iterable_of_args``. Only needed when ``iterable_of_args`` is a generator :param chunk_size: Number of simultaneous tasks to give to a worker. If ``None``, will use ``n_splits`` to determine the chunk size :param n_splits: Number of splits to use when ``chunk_size`` is ``None`` :param n_jobs: Number of workers to spawn. If ``None``, will use ``cpu_count()`` :return: Number of chunks that will be created by the chunker """ # Get number of tasks if iterable_len is not None: n_tasks = min(iterable_len, len(iterable_of_args)) if hasattr(iterable_of_args, '__len__') else iterable_len elif hasattr(iterable_of_args, '__len__'): n_tasks = len(iterable_of_args) else: raise ValueError('Failed to obtain length of iterable. Remedy: either provide an iterable with a len() ' 'function or specify iterable_len in the function call') # Determine chunk size if chunk_size is None: chunk_size = n_tasks / (n_splits or (n_jobs or cpu_count()) * 4) return min(n_tasks, math.ceil(n_tasks / chunk_size)) def make_single_arguments(iterable_of_args: Iterable, generator: bool = True) -> Union[List, Generator]: """ Converts an iterable of single arguments to an iterable of single argument tuples :param iterable_of_args: A numpy array or an iterable containing tuples of arguments to pass to a worker, which passes it to the function :param generator: Whether or not to return a generator, otherwise a materialized list will be returned :return: Iterable of single argument tuples """ gen = ((arg,) for arg in iterable_of_args) return gen if generator else list(gen) def format_seconds(seconds: Optional[Union[int, float]], with_milliseconds: bool) -> str: """ Format seconds to a string, optionally with or without milliseconds :param seconds: Number of seconds :param with_milliseconds: Whether to display milliseconds as well :return: String formatted time """ if seconds is None: return '' # Format to hours:minutes:seconds.milliseconds. Only the first 3 digits of the milliseconds is shown duration = str(timedelta(seconds=seconds)).rsplit('.', 1) if with_milliseconds and len(duration) > 1: duration = f'{duration[0]}.{duration[1][:3]}' else: duration = duration[0] return duration class TimeIt: """ Simple class that provides a context manager for keeping track of task duration and adds the total number of seconds in a designated output array """ def __init__(self, cum_time_array: Optional[SynchronizedArray], array_idx: int, max_time_array: Optional[SynchronizedArray] = None, format_args_func: Optional[Callable] = None) -> None: """ :param cum_time_array: Optional array to store cumulative time in :param array_idx: Index of cum_time_array to store the time value to :param max_time_array: Optional array to store maximum time duration in. Note that the array_idx doesn't apply to this array. The entire array is used for heapq :param format_args_func: Optional function which should return the formatted args corresponding to the function called within this context manager """ self.cum_time_array = cum_time_array self.array_idx = array_idx self.max_time_array = max_time_array self.format_args_func = format_args_func self.start_time = None def __enter__(self) -> None: self.start_time = time.time() def __exit__(self, exc_type, exc_val, exc_tb) -> None: duration = time.time() - self.start_time if self.cum_time_array is not None: self.cum_time_array[self.array_idx] += duration if self.max_time_array is not None and duration > self.max_time_array[0][0]: heapq.heappushpop(self.max_time_array, (duration, self.format_args_func() if self.format_args_func is not None else None)) def create_sync_manager(use_dill: bool) -> SyncManager: """ Create a SyncManager instance :param use_dill: Whether dill is used as serialization library :return: SyncManager instance """ authkey = os.urandom(24) return mp_dill.managers.SyncManager(authkey=authkey) if use_dill else SyncManager(authkey=authkey) class NonPickledSyncManager: """ SyncManager wrapper that won't be pickled """ def __init__(self, use_dill: bool) -> None: """ :param use_dill: Whether dill is used as serialization library """ self.manager = create_sync_manager(use_dill) def __getattr__(self, item: str): return getattr(self.manager, item) def __getstate__(self) -> dict: """ Returns the state excluding the manager object, as this is not picklable and not needed. :return: State dict """ state = self.__dict__.copy() state["manager"] = None return state def __setstate__(self, state: dict) -> None: """ Set the state. :param state: State dict """ self.__dict__ = state mpire-2.10.2/mpire/worker.py000066400000000000000000001017031461637447300157300ustar00rootroot00000000000000import collections.abc try: import dill as pickle except ImportError: import pickle import multiprocessing as mp import signal import time import traceback import _thread from functools import partial from threading import current_thread, main_thread, Thread from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union try: import multiprocess DILL_INSTALLED = True except ImportError: DILL_INSTALLED = False try: import numpy as np NUMPY_INSTALLED = True except ImportError: np = None NUMPY_INSTALLED = False from mpire.comms import (APPLY_PILL, EXIT_FUNC, INIT_FUNC, NEW_MAP_PARAMS_PILL, NON_LETHAL_POISON_PILL, POISON_PILL, WorkerComms) from mpire.context import FORK_AVAILABLE, MP_CONTEXTS, RUNNING_WINDOWS from mpire.dashboard.connection_utils import DashboardConnectionDetails, set_dashboard_connection from mpire.exception import CannotPickleExceptionError, InterruptWorker, StopWorker from mpire.insights import WorkerInsights from mpire.params import WorkerMapParams, WorkerPoolParams from mpire.tqdm_utils import TqdmConnectionDetails, TqdmManager from mpire.utils import TimeIt class AbstractWorker: """ A multiprocessing helper class which continuously asks the queue for new jobs, until a poison pill is inserted """ def __init__(self, worker_id: int, pool_params: WorkerPoolParams, map_params: WorkerMapParams, worker_comms: WorkerComms, worker_insights: WorkerInsights, tqdm_connection_details: TqdmConnectionDetails, dashboard_connection_details: DashboardConnectionDetails, start_time: float) -> None: """ :param worker_id: Worker ID :param pool_params: WorkerPool parameters :param map_params: WorkerPool map parameters :param worker_comms: Worker communication objects (queues, locks, events, ...) :param worker_insights: WorkerInsights object which stores the worker insights :param tqdm_connection_details: Tqdm manager host, and whether the manager is started/connected :param dashboard_connection_details: Dashboard manager host, port_nr and whether a dashboard is started/connected :param start_time: Timestamp indicating at what time the Worker instance was created and started """ super().__init__() # Parameters self.worker_id = worker_id self.pool_params = pool_params self.map_params = map_params self.worker_comms = worker_comms self.worker_insights = worker_insights self.tqdm_connection_details = tqdm_connection_details self.dashboard_connection_details = dashboard_connection_details self.start_time = start_time # Worker state self.worker_state = {} # Local variables needed for each worker self.additional_args = None self.progress_bar_last_updated = time.time() self.progress_bar_n_tasks_completed = 0 self.max_task_duration_last_updated = self.progress_bar_last_updated self.max_task_duration_list = self.worker_insights.get_max_task_duration_list(self.worker_id) self.is_apply_func = False self.last_job_id = None self.init_func_completed = False def run(self) -> None: """ Continuously asks the tasks queue for new task arguments. When not receiving a poisonous pill or when the max life span is not yet reached it will execute the new task and put the results in the results queue. """ # Register handlers for graceful shutdown self._set_signal_handlers() n_tasks_executed = 0 try: self.worker_comms.signal_worker_alive(self.worker_id) self.worker_comms.reset_results_received(self.worker_id) # Set tqdm and dashboard connection details. This is needed for nested pools and in the case forkserver or # spawn is used as start method TqdmManager.set_connection_details(self.tqdm_connection_details) set_dashboard_connection(self.dashboard_connection_details, auto_connect=False) # Store how long it took to start up self.worker_insights.update_start_up_time(self.worker_id, self.start_time) # Gather and set additional args to pass to the function self._set_additional_args() # Determine what function to call. If we have to keep in mind the order (for map) we use the helper function # with idx support which deals with the provided idx variable. func = self._get_func(self.map_params.func) while self.map_params.worker_lifespan is None or n_tasks_executed < self.map_params.worker_lifespan: # Obtain new chunk of jobs with TimeIt(self.worker_insights.worker_waiting_time, self.worker_id): next_chunked_args = self.worker_comms.get_task(self.worker_id) apply_func = None is_apply_func = False # Handle poison pill if next_chunked_args == POISON_PILL or next_chunked_args == NON_LETHAL_POISON_PILL: lethal = next_chunked_args == POISON_PILL self._handle_poison_pill(lethal, n_tasks_executed) if lethal: return continue # Update the map parameters of this function when new parameters are provided elif next_chunked_args == NEW_MAP_PARAMS_PILL: func = self._handle_new_map_params() if func is None: return continue # When an apply pill is received, we simply execute the function and put the result in the results queue elif next_chunked_args == APPLY_PILL: apply_func, next_chunked_args = self._handle_apply_pill() if apply_func is None: return is_apply_func = True # When we recieved None this means we need to stop because of an exception in the main process elif next_chunked_args is None: return # Execute jobs in this chunk try: job_id, next_chunked_args = next_chunked_args # Run initialization function. If it returns True it means an exception occurred and we should exit. # This is only run if the init function hasn't been run yet. if self.map_params.worker_init and self._run_init_func(): return # We only set the is_apply_func flag when we are not running the init/exit functions self.is_apply_func = is_apply_func results = [] for args in next_chunked_args: # Try to run this function and save results results_part, success, send_results, should_shut_down = self._run_func( apply_func if is_apply_func else func, job_id, args ) if should_shut_down: return if send_results: results.append((job_id, success, results_part)) # Update progress bar info if not is_apply_func: self._update_progress_bar() # Send results back to main process if results: self.worker_comms.add_results(self.worker_id, results) n_tasks_executed += len(results) # In case an exception occurred and we need to return, we want to call task_done no matter what finally: self.is_apply_func = False self.worker_comms.task_done(self.worker_id) # Update task insights self._update_task_insights() # Max lifespan reached self._update_task_insights(force_update=True) self._update_progress_bar(force_update=True) if self.map_params.worker_exit and self._run_exit_func(): return finally: # Wait until all results have been received, otherwise the main process might deadlock self.worker_comms.wait_for_all_results_received(self.worker_id) # Notify WorkerPool to start a new worker if (not self.worker_comms.exception_thrown() and self.map_params.worker_lifespan is not None and n_tasks_executed >= self.map_params.worker_lifespan): self.worker_comms.signal_worker_restart(self.worker_id) self.worker_comms.signal_worker_dead(self.worker_id) def _set_signal_handlers(self) -> None: """ Set signal handlers for graceful shutdown """ # Don't set signals when we're using threading as start method if current_thread() != main_thread(): return # When on unix, we can make use of signals if not RUNNING_WINDOWS: signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGHUP, self._on_kill_exit_gracefully) signal.signal(signal.SIGTERM, self._on_kill_exit_gracefully) signal.signal(signal.SIGUSR1, self._on_exception_exit_gracefully) # On Windows, not all signals are available so we have to work around it. elif RUNNING_WINDOWS and self.pool_params.start_method != "threading" and current_thread() == main_thread(): signal.signal(signal.SIGINT, self._on_exception_exit_gracefully) t = Thread(target=self._on_exception_exit_gracefully_windows, daemon=True) t.start() def _on_kill_exit_gracefully(self, *_) -> None: """ When someone manually sends a kill signal to this process, we want to exit gracefully. We do this by raising an exception when a task is running. Otherwise, we call raise() ourselves with the exception. Both will ensure exception_thrown() is set and will shutdown the pool. """ err = RuntimeError(f"Worker-{self.worker_id} was killed") with self.worker_comms.get_worker_running_task_lock(self.worker_id): if self.worker_comms.get_worker_running_task(self.worker_id): raise err else: self._raise(self.last_job_id, None, err) def _on_exception_exit_gracefully(self, *_) -> None: """ This function is called when the main process sends a kill signal to this process. This can mean two things: - Another child process encountered an error in either the init/exit or map function which means we should exit - The current task timed out and we should interrupt it When on Windows, this function can be invoked when no function is running. This means we will need to check if there is a running task and only raise if there is. Otherwise, the exception thrown event will be set and the worker will exit gracefully itself. On other platforms, this signal is only send when either the user defined function, worker init or worker exit function is running. In such cases, a StopWorker exception is raised, which is caught by the ``_run_safely()`` function, so we can quit gracefully. """ exception_job_id = self.worker_comms.get_exception_thrown_job_id() if RUNNING_WINDOWS: with self.worker_comms.get_worker_running_task_lock(self.worker_id): if self.worker_comms.get_worker_running_task(self.worker_id): self.worker_comms.set_worker_running_task(self.worker_id, False) if exception_job_id in {INIT_FUNC, EXIT_FUNC} or not self.is_apply_func: self.worker_comms.signal_kill_signal_received() raise StopWorker else: raise InterruptWorker else: if exception_job_id in {INIT_FUNC, EXIT_FUNC} or not self.is_apply_func: self.worker_comms.signal_kill_signal_received() raise StopWorker else: raise InterruptWorker def _on_exception_exit_gracefully_windows(self) -> None: """ Windows doesn't fully support signals as Unix-based systems do. Therefore, we have to work around it. This function is started in a thread. We wait for a kill signal (Event object) and interrupt the main thread if we got it (derived from https://stackoverflow.com/a/40281422) and only when a function is running. This will raise a KeyboardInterrupt, which is then caught by the signal handler, which in turn checks if we need to raise a StopWorker or InterruptWorker. When no function is running, the exception thrown event will be set and the worker will exit gracefully itself. Note: functions that release the GIL won't be interupted by this procedure (e.g., time.sleep). If graceful shutdown takes too long the process will be terminated by the main process. """ while self.worker_comms.is_worker_alive(self.worker_id): if self.worker_comms.wait_for_exception_thrown(timeout=0.1): with self.worker_comms.get_worker_running_task_lock(self.worker_id): if self.worker_comms.get_worker_running_task(self.worker_id): _thread.interrupt_main() return def _set_additional_args(self) -> None: """ Gather additional args to pass to the function (worker ID, shared objects, worker state) """ self.additional_args = [] if self.pool_params.pass_worker_id: self.additional_args.append(self.worker_id) if self.pool_params.shared_objects is not None: self.additional_args.append(self.pool_params.shared_objects) if self.pool_params.use_worker_state: self.additional_args.append(self.worker_state) def _get_func(self, func: Callable, is_apply_func: bool = False) -> Callable: """ Determine what function to call. If we have to keep in mind the order (for map) we use the helper function with idx support which deals with the provided idx variable. However, if we are dealing with an apply function, we ignore this as it doesn't matter. :param func: Function to call :param is_apply_func: Whether this is an apply function :return: Function to call """ helper_func = (self._helper_func_with_idx if not is_apply_func and self.worker_comms.keep_order() else self._helper_func) return partial(helper_func, partial(func, *self.additional_args)) def _handle_poison_pill(self, lethal: bool, n_tasks_executed: int) -> None: """ Force update task insights and progress bar when we got a (non-lethal) poison pill. For a lethal poison pill, we run the worker exit function if this worker actually did some work, and wait for the progress bar to be done. For a non-lethal poison pill, we simply continue. :param lethal: Whether this is a lethal poison pill """ self._update_task_insights(force_update=True) self._update_progress_bar(force_update=True) self.worker_comms.task_done(self.worker_id) if lethal: if self.map_params.worker_exit and n_tasks_executed > 0: self._run_exit_func() if self.map_params.progress_bar: self.worker_comms.wait_until_progress_bar_is_complete() def _handle_new_map_params(self) -> Optional[Callable]: """ Handle new map parameters. This means we need to update the map parameters and get the new function to call. :return: Function to call """ self.worker_comms.task_done(self.worker_id) map_params = self.worker_comms.get_task(self.worker_id) # It can happen that at the moment we get a new map params pill, an exception occurred in another process. # Therefore, get_task will return None if map_params is None: return None self.map_params = map_params func = self._get_func(self.map_params.func) self.worker_comms.task_done(self.worker_id) return func def _handle_apply_pill(self) -> Union[Tuple[Callable, Any], Tuple[None, None]]: """ Handle apply pill. This means we need to get the next task and return the function to call and the next chunked args to process :return: Function to call and next chunked args to process """ self.worker_comms.task_done(self.worker_id) task = self.worker_comms.get_task(self.worker_id) # It can happen that at the moment we get an apply pill, an exception occurred in another process. Therefore, # get_task will return None if task is None: return None, None job_id, (apply_func, args) = task func = self._get_func(apply_func, is_apply_func=True) next_chunked_args = job_id, (args,) return func, next_chunked_args def _run_init_func(self) -> bool: """ Runs the init function when provided. :return: True when the worker needs to shut down, False otherwise """ if self.init_func_completed: return False self.worker_comms.signal_worker_working_on_job(self.worker_id, INIT_FUNC) self.last_job_id = INIT_FUNC def _init_func(): with TimeIt(self.worker_insights.worker_init_time, self.worker_id): self.map_params.worker_init(*self.additional_args) # Optionally update timeout info if self.map_params.worker_init_timeout is not None: try: self.worker_comms.signal_worker_init_started(self.worker_id) _, _, _, should_shut_down = self._run_safely(_init_func, INIT_FUNC) finally: self.worker_comms.signal_worker_init_completed(self.worker_id) else: _, _, _, should_shut_down = self._run_safely(_init_func, INIT_FUNC) self.init_func_completed = True return should_shut_down def _run_func(self, func: Callable, job_id: Optional[int], args: Optional[List]) -> Tuple[Any, bool, bool, bool]: """ Runs the main function when provided. :param func: Function to call :param job_id: Job ID :param args: Args to pass to the function :return: Tuple containing results from the function and boolean values indicating whether the function was run successfully, whether the results should send on the queue, and indicating whether the worker needs to shut down """ if self.last_job_id != job_id: self.worker_comms.signal_worker_working_on_job(self.worker_id, job_id) self.last_job_id = job_id def _func(): with TimeIt(self.worker_insights.worker_working_time, self.worker_id, self.max_task_duration_list, lambda: self._format_args(args, separator=' | ')): _results = func(*args) if self.is_apply_func else func(args) self.worker_insights.update_n_completed_tasks(self.worker_id) return _results # Update timeout info try: self.worker_comms.signal_worker_task_started(self.worker_id) results, success, send_results, should_shut_down = self._run_safely(_func, job_id, args) finally: self.worker_comms.signal_worker_task_completed(self.worker_id) return results, success, send_results, should_shut_down def _run_exit_func(self) -> bool: """ Runs the exit function when provided and stores its results. :return: True when the worker needs to shut down, False otherwise """ self.worker_comms.signal_worker_working_on_job(self.worker_id, EXIT_FUNC) self.last_job_id = EXIT_FUNC def _exit_func(): with TimeIt(self.worker_insights.worker_exit_time, self.worker_id): return self.map_params.worker_exit(*self.additional_args) # Optionally update timeout info if self.map_params.worker_exit_timeout is not None: try: self.worker_comms.signal_worker_exit_started(self.worker_id) results, success, send_results, should_shut_down = self._run_safely(_exit_func, EXIT_FUNC) finally: self.worker_comms.signal_worker_exit_completed(self.worker_id) else: results, success, send_results, should_shut_down = self._run_safely(_exit_func, EXIT_FUNC) if should_shut_down: return True elif send_results: self.worker_comms.add_results(self.worker_id, [(EXIT_FUNC, True, results)]) return False def _run_safely( self, func: Callable, job_id: Optional[int], exception_args: Optional[Any] = None ) -> Tuple[Any, bool, bool, bool]: """ A rather complex locking and exception mechanism is used here so we can make sure we only raise an exception when we should. See `_exit_gracefully` for more information. :param func: Function to run :param job_id: Job ID :param exception_args: Arguments to pass to `_format_args` when an exception occurred :return: Tuple containing results from the function and boolean values indicating whether the function was run successfully, whether the results should send on the queue, and indicating whether the worker needs to shut down """ if self.worker_comms.exception_thrown(): return None, True, False, True try: try: # Obtain lock and try to run the function. During this block a StopWorker exception from the parent # process can come through to signal we should stop self.worker_comms.set_worker_running_task(self.worker_id, True) results = func() self.worker_comms.set_worker_running_task(self.worker_id, False) except InterruptWorker: # The main process tells us to interrupt the current task. This means a timeout has expired and we # need to stop this task and continue with the next one return None, False, False, False except (Exception, SystemExit) as err: # An exception occurred inside the provided function. Let the signal handler know it shouldn't raise any # StopWorker or InterruptWorker exceptions from the parent process anymore, we got this. self.worker_comms.set_worker_running_task(self.worker_id, False) if self.is_apply_func: # Obtain exception and send it back as normal results. The first False indicates the job has failed exception = self._get_exception(exception_args, err) return exception, False, True, False else: # Pass exception to parent process and stop self._raise(job_id, exception_args, err) raise StopWorker except StopWorker: # Either the main process tells us to stop working and kill all workers, or an exception occurred in this # worker and we need to stop. return None, False, False, True # Carry on return results, True, True, False def _raise(self, job_id: Optional[int], args: Optional[Any], err: Union[Exception, SystemExit]) -> None: """ Create exception and pass it to the parent process. Let other processes know an exception is set :param job_id: Job ID :param args: Funtion arguments where exception was raised :param err: Exception that should be passed on to parent process """ # Only raise an exception when this process is the first one to raise. It is technically possible that multiple # workers will get through this if statement, but that's fine, it won't cause any problems if not self.worker_comms.exception_thrown(): # Let others know we need to stop self.worker_comms.signal_exception_thrown(job_id) # Get exception and traceback string exception = self._get_exception(args, err) # Add exception self.worker_comms.add_results(self.worker_id, [(job_id, False, exception)]) def _get_exception(self, args: Optional[Any], err: Union[Exception, SystemExit]) -> Tuple[type, Tuple, Dict, str]: """ Try to pickle the exception and create a traceback string :param args: Funtion arguments where exception was raised :param err: Exception that was raised :return: Tuple containing the exception type, args, state, and a traceback string """ # Create traceback string traceback_str = f"\n\nException occurred in Worker-{self.worker_id} with the following arguments:\n" \ f"{self._format_args(args)}\n{traceback.format_exc()}" # Sometimes an exception cannot be pickled (i.e., we get the _pickle.PickleError: Can't pickle # : it's not the same object as ...). We check that here by trying the pickle.dumps manually. # The call to `queue.put` creates a thread in which it pickles and when that raises an exception we # cannot catch it. try: pickle.dumps(type(err)) pickle.dumps(err.args) pickle.dumps(err.__dict__) except (pickle.PicklingError, TypeError): err = CannotPickleExceptionError(repr(err)) return type(err), err.args, err.__dict__, traceback_str def _format_args(self, args: Optional[Any], separator: str = '\n') -> str: """ Format the function arguments to a string form. :param args: Funtion arguments :param separator: String to use as separator between arguments :return: String containing the task arguments """ # Determine function arguments if self.is_apply_func: func_args, func_kwargs = args else: func_args = args[1] if args and self.worker_comms.keep_order() else args func_kwargs = None func_args, func_kwargs = self._convert_args_kwargs(func_args, func_kwargs) # Format arguments formatted_args = [] formatted_args.extend([f"Arg {arg_nr}: {repr(arg)}" for arg_nr, arg in enumerate(func_args)]) formatted_args.extend([f"Arg {str(key)}: {repr(value)}" for key, value in func_kwargs.items()]) return separator.join(formatted_args) def _helper_func_with_idx(self, func: Callable, args: Tuple[int, Any]) -> Tuple[int, Any]: """ Helper function which calls the function `func` but preserves the order index :param func: Function to call each time new task arguments become available :param args: Tuple of ``(idx, _args)`` where ``_args`` correspond to the arguments to pass on to the function. ``idx`` is used to preserve order :return: (idx, result of calling the function with the given arguments) tuple """ return args[0], self._call_func(func, args[1]) def _helper_func(self, func: Callable, args: Any, kwargs: Optional[Dict] = None) -> Any: """ Helper function which calls the function `func` :param func: Function to call each time new task arguments become available :param args: Arguments to pass on to the function :param kwargs: Keyword arguments to pass to the function :return: Result of calling the function with the given arguments) tuple """ return self._call_func(func, args, kwargs) def _call_func(self, func: Callable, args: Any, kwargs: Optional[Dict] = None) -> Any: """ Helper function which calls the function `func` and passes the arguments in the correct way :param func: Function to call each time new task arguments become available :param args: Arguments to pass on to the function. If this is a dictionary and kwargs is not provided, then these args will be treated as keyword arguments. If this is an iterable, then the arguments will be unpacked. :param kwargs: Keyword arguments to pass to the function :return: Result of calling the function with the given arguments) tuple """ args, kwargs = self._convert_args_kwargs(args, kwargs) return func(*args, **kwargs) @staticmethod def _convert_args_kwargs(args: Any, kwargs: Optional[Dict] = None) -> Tuple[Tuple, Dict]: """ Convert the arguments to a tuple and keyword arguments to a dictionary. If args is a dictionary and kwargs is not provided, then these args will be treated as keyword arguments. If this is an iterable (but not str, bytes, or numpy array), then these arguments will be unpacked. :param args: Arguments :param kwargs: Keyword arguments :return: Args and kwargs """ if isinstance(args, dict) and kwargs is None: kwargs = args args = () elif (isinstance(args, collections.abc.Iterable) and not isinstance(args, (str, bytes)) and not (NUMPY_INSTALLED and isinstance(args, np.ndarray))): pass else: args = args, if kwargs is None: kwargs = {} return args, kwargs def _update_progress_bar(self, force_update: bool = False) -> None: """ Update the progress bar data :param force_update: Whether to force an update """ if self.map_params.progress_bar: (self.progress_bar_last_updated, self.progress_bar_n_tasks_completed) = self.worker_comms.task_completed_progress_bar( self.worker_id, self.progress_bar_last_updated, self.progress_bar_n_tasks_completed, force_update ) def _update_task_insights(self, force_update: bool = False) -> None: """ Update the task insights data :param force_update: Whether to force an update """ self.max_task_duration_last_updated = self.worker_insights.update_task_insights( self.worker_id, self.max_task_duration_last_updated, self.max_task_duration_list, force_update=force_update ) if FORK_AVAILABLE: class ForkWorker(AbstractWorker, MP_CONTEXTS['mp']['fork'].Process): pass class ForkServerWorker(AbstractWorker, MP_CONTEXTS['mp']['forkserver'].Process): pass class SpawnWorker(AbstractWorker, MP_CONTEXTS['mp']['spawn'].Process): pass class ThreadingWorker(AbstractWorker, MP_CONTEXTS['threading'].Thread): pass if DILL_INSTALLED: if FORK_AVAILABLE: class DillForkWorker(AbstractWorker, MP_CONTEXTS['mp']['fork'].Process): pass class DillForkServerWorker(AbstractWorker, MP_CONTEXTS['mp_dill']['forkserver'].Process): pass class DillSpawnWorker(AbstractWorker, MP_CONTEXTS['mp_dill']['spawn'].Process): pass def worker_factory(start_method: str, use_dill: bool) -> Type[Union[AbstractWorker, mp.Process, Thread]]: """ Returns the appropriate worker class given the start method :param start_method: What Process/Threading start method to use, see the WorkerPool constructor :param use_dill: Whether to use dill has serialization backend. Some exotic types (e.g., lambdas, nested functions) don't work well when using ``spawn`` as start method. In such cased, use ``dill`` (can be a bit slower sometimes) :return: Worker class """ if start_method == 'threading': return ThreadingWorker elif use_dill: if not DILL_INSTALLED: raise ImportError("Can't use dill as the dependency \"multiprocess\" is not installed. Use `pip install " "mpire[dill]` to install the required dependency") elif start_method == 'fork': if not FORK_AVAILABLE: raise ValueError("Start method 'fork' is not available") return DillForkWorker elif start_method == 'forkserver': if not FORK_AVAILABLE: raise ValueError("Start method 'forkserver' is not available") return DillForkServerWorker elif start_method == 'spawn': return DillSpawnWorker else: raise ValueError(f"Unknown start method with dill: '{start_method}'") else: if start_method == 'fork': if not FORK_AVAILABLE: raise ValueError("Start method 'fork' is not available") return ForkWorker elif start_method == 'forkserver': if not FORK_AVAILABLE: raise ValueError("Start method 'forkserver' is not available") return ForkServerWorker elif start_method == 'spawn': return SpawnWorker else: raise ValueError(f"Unknown start method: '{start_method}'") mpire-2.10.2/requirements.txt000066400000000000000000000001631461637447300162130ustar00rootroot00000000000000# Make requirements.txt and setup.py work together properly # https://caremad.io/2013/07/setup-vs-requirement/ -e .mpire-2.10.2/setup.cfg000066400000000000000000000001341461637447300145460ustar00rootroot00000000000000[aliases] build_docs = build_sphinx -a -b html -E --source-dir docs/ --build-dir docs/_buildmpire-2.10.2/setup.py000066400000000000000000000045561461637447300144530ustar00rootroot00000000000000from setuptools import find_packages, setup def read_description(): with open('README.rst') as file: return file.read() if __name__ == '__main__': setup( name='mpire', version='2.10.2', author='Sybren Jansen', description='A Python package for easy multiprocessing, but faster than multiprocessing', long_description=read_description(), url='https://github.com/sybrenjansen/mpire', license='MIT', packages=find_packages(exclude=['*tests*']), scripts=['bin/mpire-dashboard'], install_requires=['importlib_resources; python_version<"3.9"', 'pywin32>=301; platform_system=="Windows"', 'pygments>=2.0', 'tqdm>=4.27'], include_package_data=True, extras_require={ 'dashboard': ['flask'], 'dill': ['multiprocess; python_version<"3.11"', 'multiprocess>=0.70.15; python_version>="3.11"'], 'docs': ['docutils==0.17.1', 'sphinx==3.2.1', 'sphinx-rtd-theme==0.5.0', 'sphinx-autodoc-typehints==1.11.0', 'sphinxcontrib-images==0.9.2', 'sphinx-versions==1.0.1'], 'testing': ['ipywidgets', 'multiprocess; python_version<"3.11"', 'multiprocess>=0.70.15; python_version>="3.11"', 'numpy', 'pywin32>=301; platform_system=="Windows"', 'rich'], }, test_suite='tests', tests_require=['multiprocess', 'numpy'], classifiers=[ # Development status 'Development Status :: 5 - Production/Stable', # Supported Python versions 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', # License 'License :: OSI Approved :: MIT License', # Topic 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) mpire-2.10.2/tests/000077500000000000000000000000001461637447300140715ustar00rootroot00000000000000mpire-2.10.2/tests/__init__.py000066400000000000000000000000001461637447300161700ustar00rootroot00000000000000mpire-2.10.2/tests/test_async_result.py000066400000000000000000000332731461637447300202250ustar00rootroot00000000000000import itertools import queue import time import unittest from unittest.mock import patch, Mock from mpire.async_result import (AsyncResult, AsyncResultWithExceptionGetter, UnorderedAsyncExitResultIterator, UnorderedAsyncResultIterator) from mpire.comms import INIT_FUNC, EXIT_FUNC, MAIN_PROCESS class AsyncResultTest(unittest.TestCase): def test_init(self): """ Test that the job_id is set correctly and that the cache is updated """ with self.subTest("job_counter=0", job_id=None), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): cache = {} r = AsyncResult(cache, None, None, None, True, None) self.assertEqual(r.job_id, 0) self.assertEqual(cache, {0: r}) with self.subTest("job_counter=10", job_id=None), \ patch('mpire.async_result.job_counter', itertools.count(start=10)): cache = {} r = AsyncResult(cache, None, None, None, True, None) self.assertEqual(r.job_id, 10) self.assertEqual(cache, {10: r}) with self.subTest("job_counter=0", job_id=42), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): cache = {} r = AsyncResult(cache, None, None, 42, True, None) self.assertEqual(r.job_id, 42) self.assertEqual(cache, {42: r}) with self.subTest("job_id already exists in cache"), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): with self.assertRaises(ValueError): AsyncResult({42: None}, None, None, 42, True, None) with self.subTest(timeout=None): r = AsyncResult({}, None, None, None, True, None) self.assertIsNone(r._timeout) with self.subTest(timeout=10): r = AsyncResult({}, None, None, None, True, 10) self.assertEqual(r._timeout, 10) def test_ready(self): """ Test that the ready method returns the correct value """ r = AsyncResult({}, None, None, None, True, None) self.assertFalse(r.ready()) r._ready_event.set() self.assertTrue(r.ready()) def test_successful(self): """ Test that the successful method returns the correct value """ r = AsyncResult({}, None, None, None, True, None) # Test that the method raises a ValueError if the task is not finished yet with self.assertRaises(ValueError): r.successful() r._success = True r._ready_event.set() self.assertTrue(r.successful()) r._success = False self.assertFalse(r.successful()) def test_get_value(self): """ Test that the get method returns the correct value """ r = AsyncResult({}, None, None, None, True, None) r._value = 42 r._success = True r._ready_event.set() self.assertEqual(r.get(), 42) r._value = 1337 self.assertEqual(r.get(), 1337) def test_get_timeout(self): """ Test that the get method raises a TimeoutError if the timeout is exceeded """ r = AsyncResult({}, None, None, None, True, None) start_t = time.time() with self.assertRaises(TimeoutError): r.get(timeout=0.001) self.assertGreaterEqual(time.time() - start_t, 0.001) def test_get_error(self): """ Test that the get method raises the error if the task has failed """ r = AsyncResult({}, None, None, None, True, None) r._value = ValueError('test') r._success = False r._ready_event.set() with self.assertRaises(ValueError): r.get() def test_set_success(self): """ Test that the _set method sets the correct values if the task has succeeded """ r = AsyncResult({}, None, None, None, True, None) r._set(True, 42) self.assertTrue(r._success) self.assertEqual(r._value, 42) self.assertTrue(r._ready_event.is_set()) def test_set_exception(self): """ Test that the _set method sets the correct values if the task has failed """ value_error = ValueError('test') r = AsyncResult({}, None, None, None, True, None) r._set(False, value_error) self.assertFalse(r._success) self.assertEqual(r._value, value_error) self.assertTrue(r._ready_event.is_set()) def test_set_delete_from_cache(self): """ Test that the _set method deletes the result from the cache if the task has finished """ with self.subTest("delete"): cache = {} r = AsyncResult(cache, None, None, None, True, None) r._set(True, 42) self.assertNotIn(r.job_id, cache) with self.subTest("no_delete"): cache = {} r = AsyncResult(cache, None, None, None, False, None) r._set(True, 42) self.assertIn(r.job_id, cache) def test_callback_success(self): """ Test that the callback is called if the task has succeeded """ callback = Mock() r = AsyncResult({}, callback, None, None, True, None) r._set(True, 42) callback.assert_called_once_with(42) def test_callback_error(self): """ Test that the callback is called if the task has failed """ callback = Mock() value_error = ValueError('test') r = AsyncResult({}, None, callback, None, True, None) r._set(False, value_error) callback.assert_called_once_with(value_error) class UnorderedAsyncResultIteratorTest(unittest.TestCase): def test_init(self): """ Test that the job_id is set correctly and that the cache is updated """ with self.subTest("job_counter=0", job_id=None), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): cache = {} r = UnorderedAsyncResultIterator(cache, None, None, None) self.assertEqual(r.job_id, 0) self.assertEqual(cache, {0: r}) with self.subTest("job_counter=10", job_id=None), \ patch('mpire.async_result.job_counter', itertools.count(start=10)): cache = {} r = UnorderedAsyncResultIterator(cache, None, None, None) self.assertEqual(r.job_id, 10) self.assertEqual(cache, {10: r}) with self.subTest("job_counter=0", job_id=42), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): cache = {} r = UnorderedAsyncResultIterator(cache, None, 42, None) self.assertEqual(r.job_id, 42) self.assertEqual(cache, {42: r}) with self.subTest("job_id already exists in cache"), \ patch('mpire.async_result.job_counter', itertools.count(start=0)): with self.assertRaises(ValueError): UnorderedAsyncResultIterator({42: None}, None, 42, None) with self.subTest(timeout=None): r = UnorderedAsyncResultIterator({}, None, None, None) self.assertIsNone(r._timeout) with self.subTest(timeout=0.001): r = UnorderedAsyncResultIterator({}, None, None, 0.001) self.assertEqual(r._timeout, 0.001) def test_iter(self): """ Test that the iterator is returned """ r = UnorderedAsyncResultIterator({}, None, None, None) self.assertEqual(r, iter(r)) def test_next(self): """ Test that the next method returns the correct value """ r = UnorderedAsyncResultIterator({}, None, None, None) r._set(True, 42) r._set(True, 1337) self.assertEqual(next(r), 42) self.assertEqual(r._n_returned, 1) self.assertEqual(next(r), 1337) self.assertEqual(r._n_returned, 2) def test_next_timeout(self): """ Test that the next method raises a queue.Empty if the timeout is exceeded """ r = UnorderedAsyncResultIterator({}, None, None, None) start_t = time.time() with self.assertRaises(queue.Empty): r.next(block=True, timeout=0.001) self.assertGreaterEqual(time.time() - start_t, 0.001) with self.assertRaises(queue.Empty): r.next(block=False) def test_next_all_returned(self): """ Test that the next method raises a StopIteration if all values have been returned """ r = UnorderedAsyncResultIterator({}, 2, None, None) r._set(True, 42) r._set(True, 1337) next(r) next(r) with self.assertRaises(StopIteration): next(r) def test_set_success(self): """ Test that the _set method sets the correct values if the task has succeeded """ r = UnorderedAsyncResultIterator({}, None, None, None) r._set(True, 42) self.assertIsNone(r._exception) self.assertFalse(r._got_exception.is_set()) self.assertEqual(list(r._items), [42]) r._set(True, 1337) r._set(True, 0) self.assertEqual(list(r._items), [42, 1337, 0]) def test_set_exception(self): """ Test that the _set method sets the exception if the task has failed """ r = UnorderedAsyncResultIterator({}, None, None, None) value_error = ValueError('test') r._set(False, value_error) self.assertEqual(r._exception, value_error) self.assertTrue(r._got_exception.is_set()) self.assertEqual(list(r._items), []) def test_set_length(self): """ Test that the _set method sets the correct length if it is given """ r = UnorderedAsyncResultIterator({}, None, None, None) self.assertIsNone(r._n_tasks) r.set_length(2) self.assertEqual(r._n_tasks, 2) def test_set_length_already_set(self): """ Test that the _set method raises an ValueError if the length is already set. Setting the length to the same value should not raise an error. """ r = UnorderedAsyncResultIterator({}, 2, None, None) r.set_length(2) with self.assertRaises(ValueError): r.set_length(1) def test_get_exception(self): """ Test that the get_exception method returns the correct exception """ r = UnorderedAsyncResultIterator({}, None, None, None) value_error = ValueError('test') r._set(False, value_error) self.assertEqual(r.get_exception(), value_error) def test_remove_from_cache(self): """ Test that the remove_from_cache method removes the result from the cache """ cache = {} r = UnorderedAsyncResultIterator(cache, None, None, None) self.assertIn(r.job_id, cache) r.remove_from_cache() self.assertNotIn(r.job_id, cache) class AsyncResultWithExceptionGetterTest(unittest.TestCase): def test_init(self): """ Test that the result is initialized correctly """ r = AsyncResultWithExceptionGetter({}, INIT_FUNC) self.assertEqual(r.job_id, INIT_FUNC) self.assertFalse(r._delete_from_cache) r = AsyncResultWithExceptionGetter({}, MAIN_PROCESS) self.assertEqual(r.job_id, MAIN_PROCESS) self.assertFalse(r._delete_from_cache) def test_get_exception(self): """ Test that the get_exception method returns the correct exception """ r = AsyncResultWithExceptionGetter({}, INIT_FUNC) value_error = ValueError('test') r._set(False, value_error) self.assertEqual(r.get_exception(), value_error) def test_reset(self): """ Test that the reset method resets the result """ value_error = ValueError('test') r = AsyncResultWithExceptionGetter({}, INIT_FUNC) r._set(False, value_error) self.assertFalse(r._success) self.assertEqual(r._value, value_error) self.assertTrue(r._ready_event.is_set()) r.reset() self.assertIsNone(r._success) self.assertIsNone(r._value) self.assertFalse(r._ready_event.is_set()) class UnorderedAsyncExitResultIteratorTest(unittest.TestCase): def test_init(self): """ Test that the result is initialized correctly """ r = UnorderedAsyncExitResultIterator({}) self.assertEqual(r.job_id, EXIT_FUNC) self.assertIsNone(r._n_tasks) def test_get_results(self): """ Test that the get_results method returns the correct results """ value_error = ValueError('test') r = UnorderedAsyncExitResultIterator({}) r._set(True, 42) r._set(True, 1337) r._set(False, value_error) self.assertEqual(r.get_results(), [42, 1337]) def test_reset(self): """ Test that the reset method resets the result """ r = UnorderedAsyncExitResultIterator({}) r._set(True, 42) r._set(False, ValueError('test')) r.set_length(2) next(r) self.assertEqual(r._n_tasks, 2) self.assertEqual(len(r._items), 0) # Not 2, because 1 has alraedy been returned and 1 is an exception self.assertEqual(r._n_received, 1) self.assertEqual(r._n_returned, 1) self.assertIsNotNone(r._exception) self.assertTrue(r._got_exception.is_set()) r.reset() self.assertEqual(r._n_tasks, None) self.assertEqual(len(r._items), 0) self.assertEqual(r._n_received, 0) self.assertEqual(r._n_returned, 0) self.assertIsNone(r._exception) self.assertFalse(r._got_exception.is_set()) mpire-2.10.2/tests/test_comms.py000066400000000000000000000724641461637447300166350ustar00rootroot00000000000000import ctypes import multiprocessing as mp import queue import threading import unittest import warnings from collections import deque from datetime import datetime from itertools import product from unittest.mock import patch from mpire.comms import MAIN_PROCESS, NEW_MAP_PARAMS_PILL, NON_LETHAL_POISON_PILL, POISON_PILL, WorkerComms from mpire.context import DEFAULT_START_METHOD, FORK_AVAILABLE, MP_CONTEXTS from mpire.params import WorkerMapParams def _f1(): pass def _f2(): pass class WorkerCommsTest(unittest.TestCase): def test_init_comms(self): """ Test if initializing/resetting the comms is done properly """ test_ctx = [MP_CONTEXTS['mp_dill']['spawn'], MP_CONTEXTS['threading']] if FORK_AVAILABLE: test_ctx.extend([MP_CONTEXTS['mp_dill']['fork'], MP_CONTEXTS['mp']['forkserver']]) for ctx, n_jobs, order_tasks in product(test_ctx, [1, 2, 4], [False, True]): comms = WorkerComms(ctx, n_jobs, order_tasks) with self.subTest('__init__ called', ctx=ctx, n_jobs=n_jobs, order_tasks=order_tasks): condition_type = type(ctx.Condition(ctx.Lock())) event_type = type(ctx.Event()) lock_type = type(ctx.Lock()) value_type = type(ctx.Value('i', 0, lock=True)) self.assertEqual(comms.ctx, ctx) self.assertEqual(comms.n_jobs, n_jobs) self.assertEqual(comms.order_tasks, order_tasks) self.assertFalse(comms.is_initialized()) self.assertIsInstance(comms._keep_order, value_type) self.assertFalse(comms._keep_order.value) self.assertIsInstance(comms._task_queues, list) self.assertEqual(len(comms._task_queues), 0) self.assertIsNone(comms._task_idx) self.assertIsInstance(comms._worker_running_task, list) self.assertEqual(len(comms._worker_running_task), 0) self.assertIsInstance(comms._last_completed_task_worker_id, deque) self.assertEqual(len(comms._last_completed_task_worker_id), 0) self.assertIsNone(comms._worker_working_on_job) self.assertIsNone(comms._results_queue) self.assertIsInstance(comms._results_added, list) self.assertEqual(len(comms._results_added), 0) self.assertIsNone(comms._results_received) self.assertIsNone(comms._worker_restart_array) self.assertIsInstance(comms._worker_restart_condition, condition_type) self.assertIsNone(comms._workers_dead) self.assertIsNone(comms._workers_time_task_started) self.assertIsInstance(comms.exception_lock, lock_type) self.assertIsInstance(comms._exception_thrown, event_type) self.assertIsNone(comms._exception_job_id) self.assertIsInstance(comms._kill_signal_received, value_type) self.assertFalse(comms._exception_thrown.is_set()) self.assertFalse(comms._kill_signal_received.value) self.assertIsNone(comms._tasks_completed_array) self.assertIsNone(comms._progress_bar_last_updated) self.assertIsNone(comms._progress_bar_shutdown) self.assertIsNone(comms._progress_bar_complete) with self.subTest('without initial values', ctx=ctx, n_jobs=n_jobs, order_tasks=order_tasks), \ patch('time.time', return_value=0.0): comms.init_comms() self._check_comms_are_initialized(comms, n_jobs) # Set values so we can test if the containers will be properly resetted comms._task_idx = 4 comms._last_completed_task_worker_id.append(2) comms._last_completed_task_worker_id.append(1) comms._last_completed_task_worker_id.append(2) for i in range(n_jobs): comms._worker_running_task[i].value = i % 2 == 0 for i in range(n_jobs): comms._worker_working_on_job[i] = i + 1 comms._results_added = [i + 1 for i in range(n_jobs)] for i in range(n_jobs): comms._results_received[i] = i + 1 for i in range(n_jobs): comms._worker_restart_array[i] = i % 2 == 0 comms._workers_dead[:] = [False] * n_jobs for i in range(n_jobs): for j in range(3): comms._workers_time_task_started[i * 3 + j] = i + j + 1 comms._exception_thrown.set() comms._exception_job_id = 89 comms._kill_signal_received.value = True for i in range(n_jobs): comms._tasks_completed_array[i] = i + 1 comms._progress_bar_last_updated = 3 comms._progress_bar_shutdown.value = True comms._progress_bar_complete.set() comms.reset() with self.subTest('with initial values', ctx=ctx, n_jobs=n_jobs, order_tasks=order_tasks), \ patch('time.time', return_value=0.0): comms.init_comms() self._check_comms_are_initialized(comms, n_jobs) # Reset initialized flag comms.reset() self.assertFalse(comms.is_initialized()) def _check_comms_are_initialized(self, comms: WorkerComms, n_jobs: int) -> None: """ Checks if the WorkerComms have been properly initialized :param comms: WorkerComms object :param n_jobs: Number of jobs """ array_type = type(comms.ctx.Array('i', n_jobs, lock=True)) event_type = type(comms.ctx.Event()) joinable_queue_type = type(comms.ctx.JoinableQueue()) rlock_type = type(comms.ctx.RLock()) value_type = type(comms.ctx.Value('i', 0, lock=True)) self.assertEqual(len(comms._task_queues), n_jobs) for q in comms._task_queues: self.assertIsInstance(q, joinable_queue_type) self.assertIsInstance(comms._last_completed_task_worker_id, deque) self.assertEqual(len(comms._last_completed_task_worker_id), 0) self.assertEqual(len(comms._worker_running_task), n_jobs) for v in comms._worker_running_task: self.assertIsInstance(v, value_type) self.assertIsInstance(v.get_lock(), rlock_type) self.assertIsInstance(comms._worker_working_on_job, array_type) self.assertEqual(len(comms._worker_working_on_job), n_jobs) self.assertIsInstance(comms._results_queue, joinable_queue_type) self.assertIsInstance(comms._results_added, list) self.assertEqual(len(comms._results_added), n_jobs) self.assertIsInstance(comms._results_received, array_type) self.assertEqual(len(comms._results_received), n_jobs) self.assertIsInstance(comms._worker_restart_array, array_type) self.assertEqual(len(comms._worker_restart_array), n_jobs) self.assertIsInstance(comms._workers_dead, array_type) self.assertEqual(len(comms._workers_dead), n_jobs) self.assertIsInstance(comms._workers_time_task_started, array_type) self.assertEqual(len(comms._workers_time_task_started), n_jobs * 3) self.assertFalse(comms._exception_thrown.is_set()) self.assertIsInstance(comms._exception_job_id, value_type) self.assertEqual(comms._exception_job_id.value, 0) self.assertFalse(comms._kill_signal_received.value) self.assertIsInstance(comms._tasks_completed_array, array_type) self.assertEqual(len(comms._tasks_completed_array), n_jobs) self.assertEqual(comms._progress_bar_last_updated, 0.0) self.assertIsInstance(comms._progress_bar_shutdown, value_type) self.assertFalse(comms._progress_bar_shutdown.value) self.assertIsInstance(comms._progress_bar_complete, event_type) self.assertFalse(comms._progress_bar_complete.is_set()) self.assertTrue(comms.is_initialized()) # Basic sanity checks for the values self.assertEqual(comms._task_idx, 0) self.assertEqual([v.value for v in comms._worker_running_task], [False] * n_jobs) self.assertEqual(comms._worker_working_on_job[:], [0] * n_jobs) self.assertEqual(comms._results_received[:], [0] * n_jobs) self.assertEqual(comms._worker_restart_array[:], [False] * n_jobs) self.assertEqual(comms._workers_dead[:], [True] * n_jobs) self.assertEqual(comms._workers_time_task_started[:], [0.0] * n_jobs * 3) self.assertEqual(comms._tasks_completed_array[:], [0] * n_jobs) def test_progress_bar(self): """ Test progress bar related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) comms.init_comms() # Nothing available yet self.assertEqual(sum(comms._tasks_completed_array), 0) # 3 task done, but not enough time has passed to send the update last_updated = 0.0 n_tasks_completed = 0 with patch('time.time', return_value=0.0): for n in range(1, 4): last_updated, n_tasks_completed = comms.task_completed_progress_bar(0, last_updated, n_tasks_completed, force_update=False) self.assertEqual(n_tasks_completed, n) self.assertEqual(sum(comms._tasks_completed_array), 0) # Not enough time has passed, but we'll force the update. Number of tasks done should still be 3 with patch('time.time', return_value=0.0): last_updated, n_tasks_completed = comms.task_completed_progress_bar(0, last_updated, n_tasks_completed, force_update=True) self.assertEqual(comms.get_tasks_completed_progress_bar(), 3) self.assertEqual(last_updated, 0.0) self.assertEqual(n_tasks_completed, 0) # 4 tasks already done and another 4 tasks done. Enough time should've passed for each update call, except the # second. In total we have 3 (from above) + 4 + 4 = 11 tasks done last_updated = 0.0 n_tasks_completed = 4 with patch('time.time', side_effect=[1.0, 1.0, 3.0, 4.0]): for expected_last_updated in [1.0, 1.0, 3.0, 4.0]: last_updated, n_tasks_completed = comms.task_completed_progress_bar(0, last_updated, n_tasks_completed, force_update=False) self.assertEqual(last_updated, expected_last_updated) self.assertEqual(comms.get_tasks_completed_progress_bar(), 11) self.assertEqual(last_updated, 4.0) self.assertEqual(n_tasks_completed, 0) # Signal shutdown comms.signal_progress_bar_shutdown() self.assertEqual(comms.get_tasks_completed_progress_bar(), POISON_PILL) comms._progress_bar_shutdown.value = False # Set exception comms.signal_exception_thrown(MAIN_PROCESS) self.assertEqual(comms.get_tasks_completed_progress_bar(), POISON_PILL) comms._exception_thrown.clear() # Set kill signal received comms.signal_kill_signal_received() self.assertEqual(comms.get_tasks_completed_progress_bar(), POISON_PILL) comms._kill_signal_received.value = False def test_progress_bar_shutdown(self): """ Test progress bar complete related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) comms.init_comms() self.assertFalse(comms._progress_bar_shutdown.value) comms.signal_progress_bar_shutdown() self.assertTrue(comms._progress_bar_shutdown.value) def test_progress_bar_complete(self): """ Test progress bar complete related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) comms.init_comms() self.assertFalse(comms._progress_bar_complete.is_set()) comms.signal_progress_bar_complete() self.assertTrue(comms._progress_bar_complete.is_set()) with patch.object(comms._progress_bar_complete, 'wait') as p: comms.wait_until_progress_bar_is_complete() self.assertEqual(p.call_count, 1) def test_keep_order(self): """ Test keep_order related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) self.assertFalse(comms.keep_order()) comms.signal_keep_order() self.assertTrue(comms.keep_order()) comms.clear_keep_order() self.assertFalse(comms.keep_order()) def test_tasks(self): """ Test task related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 3, False) comms.init_comms() # Nothing available yet for worker_id in range(3): with self.assertRaises(queue.Empty): comms._task_queues[worker_id].get(block=False) # Add a few tasks. As no worker has completed tasks yet, it should give the task to workers in order job_id = 0 comms.add_task(job_id, 12) comms.add_task(job_id, 'hello world') comms.add_task(job_id, {'foo': 'bar'}) comms.add_task(job_id, {'foo': 'baz'}) comms.add_task(job_id, 34.43) comms.add_task(job_id, datetime(2000, 1, 1, 1, 2, 3)) tasks = [] for worker_id in [0, 1, 2, 0, 1, 2]: tasks.append(comms.get_task(worker_id)) comms.task_done(worker_id) self.assertListEqual(tasks, [(job_id, task) for task in [12, 'hello world', {'foo': 'bar'}, {'foo': 'baz'}, 34.43, datetime(2000, 1, 1, 1, 2, 3)]]) # When order_tasks is set it should give the tasks to the workers in the order they were added, independent of # workers that have completed tasks. If set to False and workers have completed tasks, it depends on the last # one who gets the new task. After giving a task to that worker the worker ID that last completed a task is # reset again. So, it should continue with the normal order job_id = 1 for order_tasks, worker_order in [(False, [0, 1, 2, 2, 1, 0, 2, 0, 1]), (True, [0, 1, 2, 0, 1, 2, 0, 1, 2])]: with self.subTest(order_tasks=order_tasks): comms.order_tasks = order_tasks comms.init_comms() comms.add_task(job_id, 12) comms.add_task(job_id, 'hello world') comms.add_task(job_id, {'foo': 'bar'}) comms._last_completed_task_worker_id.append(2) comms.add_task(job_id, {'foo': 'baz'}) comms._last_completed_task_worker_id.append(1) comms.add_task(job_id, 34.43) comms._last_completed_task_worker_id.append(0) comms.add_task(job_id, datetime(2000, 1, 1, 1, 2, 3)) comms._last_completed_task_worker_id.append(2) comms.add_task(job_id, '123') comms.add_task(job_id, 123) comms.add_task(job_id, 1337) tasks = [] for worker_id in worker_order: tasks.append(comms.get_task(worker_id)) comms.task_done(worker_id) self.assertListEqual(tasks, [(job_id, task) for task in [12, 'hello world', {'foo': 'bar'}, {'foo': 'baz'}, 34.43, datetime(2000, 1, 1, 1, 2, 3), '123', 123, 1337]]) # Throw in an exception. Should return None comms.signal_exception_thrown(job_id) for worker_id in range(3): self.assertIsNone(comms.get_task(worker_id)) # Should be joinable comms.join_task_queues() def test_worker_running_task(self): """ Tests that the worker_running_task functions work as expected """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 3, False) comms.init_comms() comms.set_worker_running_task(0, True) comms.set_worker_running_task(1, False) comms.set_worker_running_task(2, True) self.assertTrue(comms.get_worker_running_task(0)) self.assertFalse(comms.get_worker_running_task(1)) self.assertTrue(comms.get_worker_running_task(2)) comms.set_worker_running_task(0, False) comms.set_worker_running_task(1, True) self.assertFalse(comms.get_worker_running_task(0)) self.assertTrue(comms.get_worker_running_task(1)) self.assertTrue(comms.get_worker_running_task(2)) def test_worker_working_on_job(self): """ Tests that the worker_working_on_job function works as expected """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 3, False) comms.init_comms() comms.signal_worker_working_on_job(0, 1) comms.signal_worker_working_on_job(1, 8) comms.signal_worker_working_on_job(2, 19) self.assertEqual(comms.get_worker_working_on_job(0), 1) self.assertEqual(comms.get_worker_working_on_job(1), 8) self.assertEqual(comms.get_worker_working_on_job(2), 19) comms.signal_worker_working_on_job(1, 3) comms.signal_worker_working_on_job(2, 0) comms.signal_worker_working_on_job(0, -1) self.assertEqual(comms.get_worker_working_on_job(0), -1) self.assertEqual(comms.get_worker_working_on_job(1), 3) self.assertEqual(comms.get_worker_working_on_job(2), 0) def test_results(self): """ Test results related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) comms.init_comms() # Nothing available yet with self.assertRaises(queue.Empty): comms._results_queue.get(block=False) # Add a few results. Note that `get_results` calls `task_done` comms.add_results(0, [(0, True, 12)]) comms.add_results(1, [(1, True, 'hello world')]) comms.add_results(1, [(1, False, {'foo': 'bar'})]) comms.add_results(0, [(2, True, '123')]) self.assertEqual(comms.get_results(), [(0, True, 12)]) self.assertEqual(comms._last_completed_task_worker_id.popleft(), 0) self.assertEqual(comms.get_results(), [(1, True, 'hello world')]) self.assertEqual(comms._last_completed_task_worker_id.popleft(), 1) self.assertEqual(comms.get_results(), [(1, False, {'foo': 'bar'})]) self.assertEqual(comms._last_completed_task_worker_id.popleft(), 1) self.assertEqual(comms.get_results(), [(2, True, '123')]) self.assertEqual(comms._last_completed_task_worker_id.popleft(), 0) # Should be joinable. When using keep_alive, it should still be open comms.join_results_queues(keep_alive=True) comms.add_results(0, [(2, True, 12)]) comms.get_results() comms.join_results_queues(keep_alive=False) # Depending on Python version it either throws AssertionError or ValueError with self.assertRaises((AssertionError, ValueError)): comms.add_results(0, [(2, True, 12)]) def test_add_new_map_params(self): """ Test new map parameters function """ comms = WorkerComms(MP_CONTEXTS['mp_dill'][DEFAULT_START_METHOD], 2, False) comms.init_comms() map_params = WorkerMapParams(_f1, None, None, 1) comms.add_new_map_params(map_params) with warnings.catch_warnings(): warnings.simplefilter("ignore") for worker_id in range(2): self.assertEqual(comms.get_task(worker_id), NEW_MAP_PARAMS_PILL) self.assertEqual(comms.get_task(worker_id), map_params) comms.task_done(worker_id) comms.task_done(worker_id) map_params = WorkerMapParams(_f1, _f2, None, None) comms.add_new_map_params(map_params) with warnings.catch_warnings(): warnings.simplefilter("ignore") for worker_id in range(2): self.assertEqual(comms.get_task(worker_id), NEW_MAP_PARAMS_PILL) self.assertEqual(comms.get_task(worker_id), map_params) comms.task_done(worker_id) comms.task_done(worker_id) def test_exception_thrown(self): """ Test exception thrown related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) comms.init_comms() self.assertFalse(comms.exception_thrown()) comms.signal_exception_thrown(0) self.assertTrue(comms.exception_thrown()) self.assertEqual(comms.get_exception_thrown_job_id(), 0) comms._exception_thrown.clear() self.assertFalse(comms.exception_thrown()) comms.signal_exception_thrown(13) self.assertTrue(comms.exception_thrown()) self.assertEqual(comms.get_exception_thrown_job_id(), 13) def test_kill_signal_received(self): """ Test kill signal received related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) self.assertFalse(comms.kill_signal_received()) comms.signal_kill_signal_received() self.assertTrue(comms.kill_signal_received()) comms._kill_signal_received.value = False self.assertFalse(comms.kill_signal_received()) def test_worker_poison_pill(self): """ Test that a poison pill is inserted for every worker """ for n_jobs in [1, 2, 4]: with self.subTest(n_jobs=n_jobs): comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], n_jobs, False) comms.init_comms() comms.insert_poison_pill() for worker_id in range(n_jobs): self.assertEqual(comms.get_task(worker_id), POISON_PILL) comms.task_done(worker_id) comms.join_task_queues() def test_worker_non_lethal_poison_pill(self): """ Test that a non-lethal poison pill is inserted for every worker """ # Shouldn't be the same thing self.assertNotEqual(POISON_PILL, NON_LETHAL_POISON_PILL) for n_jobs in [1, 2, 4]: with self.subTest(n_jobs=n_jobs): comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], n_jobs, False) comms.init_comms() comms.insert_non_lethal_poison_pill() for worker_id in range(n_jobs): self.assertEqual(comms.get_task(worker_id), NON_LETHAL_POISON_PILL) comms.task_done(worker_id) comms.join_task_queues() def test_worker_restart(self): """ Test worker restart related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 5, False) comms.init_comms() # No restarts yet with patch.object(comms._worker_restart_condition, 'wait'): self.assertListEqual(list(comms.get_worker_restarts()), []) # Signal some restarts comms.signal_worker_restart(0) comms.signal_worker_restart(2) comms.signal_worker_restart(3) # Restarts available self.assertListEqual(list(comms.get_worker_restarts()), [0, 2, 3]) # Reset some comms.reset_worker_restart(0) comms.reset_worker_restart(3) # Restarts available self.assertListEqual(list(comms.get_worker_restarts()), [2]) # Reset last one comms.reset_worker_restart(2) with patch.object(comms._worker_restart_condition, 'wait'): self.assertListEqual(list(comms.get_worker_restarts()), []) def test_worker_alive(self): """ Test worker alive related functions """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 5, False) comms.init_comms() # Signal some workers are alive comms.signal_worker_alive(0) comms.signal_worker_alive(1) comms.signal_worker_dead(1) comms.signal_worker_alive(2) comms.signal_worker_alive(3) # Check alive status self.assertListEqual([comms.is_worker_alive(worker_id) for worker_id in range(5)], [True, False, True, True, False]) # Reset some comms.signal_worker_dead(0) comms.signal_worker_dead(3) # Check alive status self.assertListEqual([comms.is_worker_alive(worker_id) for worker_id in range(5)], [False, False, True, False, False]) def test_drain_result_queue_terminate_worker(self): """ get_results should be called once, get_exit_results should be called when exit function is defined """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 5, False) comms.init_comms() dont_wait_event = threading.Event() dont_wait_event.set() with patch.object(comms, 'get_results', side_effect=comms.get_results) as p: comms.drain_results_queue_terminate_worker(dont_wait_event) self.assertEqual(p.call_count, 1) self.assertTrue(dont_wait_event.is_set()) def test_drain_queues(self): """ _drain_and_join_queue should be called for every queue that matters. There are as many tasks queues as workers and 1 results queue """ for n_jobs in [1, 2, 4]: comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], n_jobs, False) comms.init_comms() with self.subTest(n_jobs=n_jobs), patch.object(comms, 'drain_and_join_queue') as p: comms.drain_queues() self.assertEqual(p.call_count, n_jobs + 1) def test__drain_and_join_queue(self): """ Test draining queues """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 2, False) # Create a custom queue with some data q = mp.JoinableQueue() q.put(1) q.put('hello') q.put('world') # Drain queue. It should now be closed comms._drain_and_join_queue(q) # Depending on Python version it either throws OSError or ValueError with self.assertRaises((OSError, ValueError)): q.get(block=False) def test_timeouts(self): """ Tests timeout related function """ comms = WorkerComms(MP_CONTEXTS['mp'][DEFAULT_START_METHOD], 5, False) comms.init_comms() # Signal workers started for worker_id in range(5): with self.subTest(worker_id=worker_id), patch('time.time', side_effect=[1000.0, 2000.0, 3000.0]): self.assertListEqual( comms._workers_time_task_started[worker_id * 3 : worker_id * 3 + 3], [0.0, 0.0, 0.0] ) comms.signal_worker_init_started(worker_id) comms.signal_worker_task_started(worker_id) comms.signal_worker_exit_started(worker_id) self.assertListEqual( comms._workers_time_task_started[worker_id * 3 : worker_id * 3 + 3], [1000.0, 2000.0, 3000.0] ) # Check timeouts for worker_id in range(5): # worker_init, times out at > 10 for timeout, has_timed_out in [(8, True), (9, True), (10, True), (11, False)]: with self.subTest(timeout=timeout, worker_id=worker_id), patch('time.time', return_value=1010.0): self.assertEqual(comms.has_worker_init_timed_out(worker_id, timeout), has_timed_out) # task, times out at > 9 for timeout, has_timed_out in [(8, True), (9, True), (10, False), (11, False)]: with self.subTest(timeout=timeout, worker_id=worker_id), patch('time.time', return_value=2009.0): self.assertEqual(comms.has_worker_task_timed_out(worker_id, timeout), has_timed_out) # worker_exit, times out at > 8 for timeout, has_timed_out in [(8, True), (9, False), (10, False), (11, False)]: with self.subTest(timeout=timeout, worker_id=worker_id), patch('time.time', return_value=3008.0): self.assertEqual(comms.has_worker_exit_timed_out(worker_id, timeout), has_timed_out) # Reset for worker_id in range(5): with self.subTest(worker_id=worker_id): comms.signal_worker_init_completed(worker_id) self.assertListEqual( comms._workers_time_task_started[worker_id * 3 : worker_id * 3 + 3], [0.0, 2000.0, 3000.0] ) comms.signal_worker_task_completed(worker_id) self.assertListEqual( comms._workers_time_task_started[worker_id * 3 : worker_id * 3 + 3], [0.0, 0.0, 3000.0] ) comms.signal_worker_exit_completed(worker_id) self.assertListEqual( comms._workers_time_task_started[worker_id * 3 : worker_id * 3 + 3], [0.0, 0.0, 0.0] ) # Check timeouts. Should be False because nothing started for worker_id in range(5): for timeout in [0, 0.1, 3]: with self.subTest(worker_id=worker_id, timeout=timeout): self.assertFalse(comms.has_worker_init_timed_out(worker_id, timeout)) mpire-2.10.2/tests/test_insights.py000066400000000000000000000533021461637447300173350ustar00rootroot00000000000000import ctypes import multiprocessing as mp import unittest import warnings from multiprocessing import managers as mp_managers from time import sleep from unittest.mock import patch from multiprocess import managers as mp_dill_managers from tqdm import tqdm from mpire import WorkerPool from mpire.context import DEFAULT_START_METHOD, FORK_AVAILABLE, RUNNING_WINDOWS from mpire.insights import WorkerInsights from mpire.utils import NonPickledSyncManager # Skip start methods that use fork if it's not available if not FORK_AVAILABLE: TEST_START_METHODS = ['spawn', 'threading'] else: TEST_START_METHODS = ['fork', 'forkserver', 'spawn', 'threading'] def square(barrier, x): # Wait until all workers are ready barrier.wait() # time.sleep is added for Windows compatibility, otherwise it says 0.0 time has passed sleep(0.001) return x * x class WorkerInsightsTest(unittest.TestCase): def test_reset_insights(self): """ Test if resetting the insights is done properly """ for n_jobs, use_dill in [(1, False), (2, True), (4, False)]: insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs, use_dill) self.assertEqual(insights.ctx, mp.get_context(DEFAULT_START_METHOD)) self.assertEqual(insights.n_jobs, n_jobs) with self.subTest('initialized', n_jobs=n_jobs): self.assertFalse(insights.insights_enabled) self.assertIsNone(insights.insights_manager) self.assertIsNone(insights.insights_manager_lock) self.assertIsNone(insights.worker_start_up_time) self.assertIsNone(insights.worker_init_time) self.assertIsNone(insights.worker_n_completed_tasks) self.assertIsNone(insights.worker_waiting_time) self.assertIsNone(insights.worker_working_time) self.assertIsNone(insights.worker_exit_time) self.assertIsNone(insights.max_task_duration) self.assertIsNone(insights.max_task_args) # Containers should be properly initialized with self.subTest('without initial values', n_jobs=n_jobs, enable_insights=True): insights.reset_insights(enable_insights=True) self.assertTrue(insights.insights_enabled) self.assertIsInstance(insights.insights_manager_lock, mp.synchronize.Lock) self.assertIsInstance(insights.insights_manager, NonPickledSyncManager) self.assertIsInstance(insights.worker_start_up_time, ctypes.Array) self.assertIsInstance(insights.worker_init_time, ctypes.Array) self.assertIsInstance(insights.worker_n_completed_tasks, ctypes.Array) self.assertIsInstance(insights.worker_waiting_time, ctypes.Array) self.assertIsInstance(insights.worker_working_time, ctypes.Array) self.assertIsInstance(insights.worker_exit_time, ctypes.Array) self.assertIsInstance(insights.max_task_duration, ctypes.Array) self.assertIsInstance(insights.max_task_args, mp_dill_managers.ListProxy if use_dill else mp_managers.ListProxy) # Basic sanity checks for the values self.assertEqual(sum(insights.worker_start_up_time), 0) self.assertEqual(sum(insights.worker_init_time), 0) self.assertEqual(sum(insights.worker_n_completed_tasks), 0) self.assertEqual(sum(insights.worker_waiting_time), 0) self.assertEqual(sum(insights.worker_working_time), 0) self.assertEqual(sum(insights.worker_exit_time), 0) self.assertEqual(sum(insights.max_task_duration), 0) self.assertListEqual(list(insights.max_task_args), [''] * n_jobs * 5) # Set some values so we can test if the containers will be properly resetted insights.worker_start_up_time[0] = 1 insights.worker_init_time[0] = 2 insights.worker_n_completed_tasks[0] = 3 insights.worker_waiting_time[0] = 4 insights.worker_working_time[0] = 5 insights.worker_exit_time[0] = 6 insights.max_task_duration[0] = 7 insights.max_task_args[0] = '8' # Containers should be properly initialized with self.subTest('with initial values', n_jobs=n_jobs, enable_insights=True): insights.reset_insights(enable_insights=True) # Basic sanity checks for the values self.assertEqual(sum(insights.worker_start_up_time), 0) self.assertEqual(sum(insights.worker_init_time), 0) self.assertEqual(sum(insights.worker_n_completed_tasks), 0) self.assertEqual(sum(insights.worker_waiting_time), 0) self.assertEqual(sum(insights.worker_working_time), 0) self.assertEqual(sum(insights.worker_exit_time), 0) self.assertEqual(sum(insights.max_task_duration), 0) self.assertListEqual(list(insights.max_task_args), [''] * n_jobs * 5) # Disabling should set things to None again with self.subTest(n_jobs=n_jobs, enable_insights=False): insights.reset_insights(enable_insights=False) self.assertFalse(insights.insights_enabled) self.assertIsNone(insights.insights_manager) self.assertIsNone(insights.insights_manager_lock) self.assertIsNone(insights.worker_start_up_time) self.assertIsNone(insights.worker_init_time) self.assertIsNone(insights.worker_n_completed_tasks) self.assertIsNone(insights.worker_waiting_time) self.assertIsNone(insights.worker_working_time) self.assertIsNone(insights.worker_exit_time) self.assertIsNone(insights.max_task_duration) self.assertIsNone(insights.max_task_args) def test_enable_insights(self): """ Insight containers are initially set to None values. When enabled they should be changed to appropriate containers. When a second task is started it should reset them. If disabled, they should remain None """ with warnings.catch_warnings(): warnings.simplefilter("ignore") print() for start_method in tqdm(TEST_START_METHODS): with WorkerPool(n_jobs=2, start_method=start_method, enable_insights=True) as pool: # We run this a few times to see if it resets properly. We only verify this by checking the # n_completed_tasks for idx in range(3): with self.subTest('enabled', idx=idx, start_method=start_method): # We add a barrier so we know that all workers are ready. After that, the workers start # working. Additionally, we set chunk size to 1, max tasks active to 2, and have a # time.sleep in the get_tasks function, so we know for sure that there will be waiting time barrier = pool.ctx.Barrier(2) pool.set_shared_objects(barrier) pool.map(square, self._get_tasks(10), worker_init=self._init, worker_exit=self._exit, max_tasks_active=2, chunk_size=1) # Basic sanity checks for the values. For some reason, testing Windows on Github Actions # can sometimes lead to zero start up time. Additionally, some max task args can be empty, # in that case the duration should be 0 (= no data) if RUNNING_WINDOWS: self.assertGreaterEqual(sum(pool._worker_insights.worker_start_up_time), 0) else: self.assertGreater(sum(pool._worker_insights.worker_start_up_time), 0) self.assertGreater(sum(pool._worker_insights.worker_init_time), 0) self.assertEqual(sum(pool._worker_insights.worker_n_completed_tasks), 10) self.assertGreater(sum(pool._worker_insights.worker_waiting_time), 0) self.assertGreater(sum(pool._worker_insights.worker_working_time), 0) self.assertGreater(sum(pool._worker_insights.worker_exit_time), 0) self.assertGreater(max(pool._worker_insights.max_task_duration), 0) for duration, args in zip(pool._worker_insights.max_task_duration, pool._worker_insights.max_task_args): if duration == 0: self.assertEqual(args, '') else: self.assertIn(args, {'Arg 0: 0', 'Arg 0: 1', 'Arg 0: 2', 'Arg 0: 3', 'Arg 0: 4', 'Arg 0: 5', 'Arg 0: 6', 'Arg 0: 7', 'Arg 0: 8', 'Arg 0: 9'}) with WorkerPool(n_jobs=2, enable_insights=False) as pool: # Disabling should set things to None again with self.subTest('disable', start_method=start_method): barrier = pool.ctx.Barrier(2) pool.set_shared_objects(barrier) pool.map(square, range(10)) self.assertIsNone(pool._worker_insights.insights_manager) self.assertIsNone(pool._worker_insights.insights_manager_lock) self.assertIsNone(pool._worker_insights.worker_start_up_time) self.assertIsNone(pool._worker_insights.worker_init_time) self.assertIsNone(pool._worker_insights.worker_n_completed_tasks) self.assertIsNone(pool._worker_insights.worker_waiting_time) self.assertIsNone(pool._worker_insights.worker_working_time) self.assertIsNone(pool._worker_insights.worker_exit_time) self.assertIsNone(pool._worker_insights.max_task_duration) self.assertIsNone(pool._worker_insights.max_task_args) def test_get_max_task_duration_list(self): """ Test that the right containers are selected given a worker ID """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=5, use_dill=False) with self.subTest(insights_enabled=False): for worker_id in range(5): self.assertIsNone(insights.get_max_task_duration_list(worker_id)) with self.subTest(insights_enabled=True): insights.reset_insights(enable_insights=True) insights.max_task_duration[:] = range(25) insights.max_task_args[:] = map(str, range(25)) for worker_id, expected_task_duration_list in [ (0, [(0.0, '0'), (1.0, '1'), (2.0, '2'), (3.0, '3'), (4.0, '4')]), (1, [(5.0, '5'), (6.0, '6'), (7.0, '7'), (8.0, '8'), (9.0, '9')]), (4, [(20.0, '20'), (21.0, '21'), (22.0, '22'), (23.0, '23'), (24.0, '24')]) ]: with self.subTest(worker_id=worker_id): self.assertListEqual(insights.get_max_task_duration_list(worker_id), expected_task_duration_list) def test_update_start_up_time(self): """ Test that the start up time is correctly added to worker_start_up_time for the right index """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=5, use_dill=False) # Shouldn't do anything when insights haven't been enabled with self.subTest(insights_enabled=False), patch('time.time', side_effect=[1.0, 2.0, 3.0, 7.0, 8.0]): for worker_id in range(5): insights.update_start_up_time(worker_id, 1.0) self.assertIsNone(insights.worker_start_up_time) insights.reset_insights(enable_insights=True) with self.subTest(insights_enabled=True), patch('time.time', side_effect=[1.0, 2.0, 3.0, 7.0, 8.0]): for worker_id in range(5): insights.update_start_up_time(worker_id, 1.0) self.assertListEqual(list(insights.worker_start_up_time), [0.0, 1.0, 2.0, 6.0, 7.0]) def test_update_n_completed_tasks(self): """ Test that the number of completed tasks is correctly added to worker_n_completed_tasks for the right index """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=5, use_dill=False) # Shouldn't do anything when insights haven't been enabled with self.subTest(insights_enabled=False): for worker_id, call_n_times in [(0, 1), (1, 0), (2, 5), (3, 8), (4, 11)]: for _ in range(call_n_times): insights.update_n_completed_tasks(worker_id) self.assertIsNone(insights.worker_n_completed_tasks) with self.subTest(insights_enabled=True): insights.reset_insights(enable_insights=True) for worker_id, call_n_times in [(0, 1), (1, 0), (2, 5), (3, 8), (4, 11)]: for _ in range(call_n_times): insights.update_n_completed_tasks(worker_id) self.assertListEqual(list(insights.worker_n_completed_tasks), [1, 0, 5, 8, 11]) def test_update_task_insights_not_forced(self): """ Test whether the update_task_insights is triggered correctly when ``force_update=False``. """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=5, use_dill=False) max_task_duration_last_updated = 1.0 # Shouldn't do anything when insights haven't been enabled with self.subTest(insights_enabled=False), \ patch('mpire.insights.time.time', side_effect=[0.0, 2.0, 3.0, 7.0, 8.0]): for worker_id in range(5): max_task_duration_list = insights.get_max_task_duration_list(worker_id) insights.update_task_insights(worker_id, max_task_duration_last_updated, max_task_duration_list, force_update=False) self.assertIsNone(insights.max_task_duration) self.assertIsNone(insights.max_task_args) insights.reset_insights(enable_insights=True) max_task_duration_last_updated = 1.0 # The first three worker IDs won't send an update because the two seconds hasn't passed yet. with self.subTest(insights_enabled=True), patch('time.time', side_effect=[0.0, 2.0, 3.0, 7.0, 8.0]): last_updated_times = [] for worker_id, max_task_duration_list in [ (0, [(0.1, '0'), (0.2, '1'), (0.3, '2'), (0.4, '3'), (0.5, '4')]), (1, [(1.1, '0'), (1.2, '1'), (1.3, '2'), (1.4, '3'), (1.5, '4')]), (2, [(2.1, '0'), (2.2, '1'), (2.3, '2'), (2.4, '3'), (2.5, '4')]), (3, [(3.1, '0'), (3.2, '1'), (3.3, '2'), (3.4, '3'), (3.5, '4')]), (4, [(4.1, '0'), (4.2, '1'), (4.3, '2'), (4.4, '3'), (4.5, '4')]) ]: last_updated_times.append(insights.update_task_insights( worker_id, max_task_duration_last_updated, max_task_duration_list, force_update=False )) self.assertListEqual(last_updated_times, [1.0, 1.0, 1.0, 7.0, 8.0]) self.assertListEqual(list(insights.max_task_duration), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.3, 4.4, 4.5]) self.assertListEqual(list(insights.max_task_args), ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '1', '2', '3', '4', '0', '1', '2', '3', '4']) def test_update_task_insights_forced(self): """ Test whether task insights are being updated correctly """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=2, use_dill=False) max_task_duration_last_updated = 0.0 # Shouldn't do anything when insights haven't been enabled with self.subTest(insights_enabled=False), patch('time.time', side_effect=[1.0, 2.0]): for worker_id in range(2): max_task_duration_last_updated = insights.update_task_insights( worker_id, max_task_duration_last_updated, [(0.1, '1'), (0.2, '2')], force_update=True ) self.assertIsNone(insights.max_task_duration) self.assertIsNone(insights.max_task_args) self.assertEqual(max_task_duration_last_updated, 0.0) insights.reset_insights(enable_insights=True) max_task_duration_last_updated = 0.0 with self.subTest(insights_enabled=True), patch('time.time', side_effect=[1, 2]): for worker_id, max_task_duration_list in [ (0, [(5.0, '5'), (6.0, '6'), (7.0, '7'), (8.0, '8'), (9.0, '9')]), (1, [(0.0, '0'), (1.0, '1'), (2.0, '2'), (3.0, '3'), (4.0, '4')]) ]: self.assertEqual(insights.update_task_insights( worker_id, max_task_duration_last_updated, max_task_duration_list, force_update=True ), worker_id + 1) self.assertListEqual(list(insights.max_task_duration), [5.0, 6.0, 7.0, 8.0, 9.0, 0.0, 1.0, 2.0, 3.0, 4.0]) self.assertListEqual(list(insights.max_task_args), ['5', '6', '7', '8', '9', '0', '1', '2', '3', '4']) def test_get_insights(self): """ Test if the insights are properly processed """ insights = WorkerInsights(mp.get_context(DEFAULT_START_METHOD), n_jobs=2, use_dill=False) with self.subTest(enable_insights=False): insights.reset_insights(enable_insights=False) self.assertDictEqual(insights.get_insights(), {}) with self.subTest(enable_insights=True): insights.reset_insights(enable_insights=True) insights.worker_start_up_time[:] = [0.1, 0.2] insights.worker_init_time[:] = [0.11, 0.22] insights.worker_n_completed_tasks[:] = [2, 3] insights.worker_waiting_time[:] = [0.4, 0.3] insights.worker_working_time[:] = [42.0, 37.0] insights.worker_exit_time[:] = [0.33, 0.44] # Durations that are zero or args that are empty are skipped insights.max_task_duration[:] = [0.0, 0.0, 1.0, 2.0, 0.0, 6.0, 0.8, 0.0, 0.1, 0.0] insights.max_task_args[:] = ['', '', '1', '2', '', '3', '4', '', '5', ''] insights_dict = insights.get_insights() # Test ratios separately because of rounding errors total_time = 0.3 + 0.33 + 0.7 + 79.0 + 0.77 self.assertAlmostEqual(insights_dict['start_up_ratio'], 0.3 / total_time) self.assertAlmostEqual(insights_dict['init_ratio'], 0.33 / total_time) self.assertAlmostEqual(insights_dict['waiting_ratio'], 0.7 / total_time) self.assertAlmostEqual(insights_dict['working_ratio'], 79.0 / total_time) self.assertAlmostEqual(insights_dict['exit_ratio'], 0.77 / total_time) del (insights_dict['start_up_ratio'], insights_dict['init_ratio'], insights_dict['waiting_ratio'], insights_dict['working_ratio'], insights_dict['exit_ratio']) self.assertDictEqual(insights_dict, { 'n_completed_tasks': [2, 3], 'start_up_time': ['0:00:00.100', '0:00:00.200'], 'init_time': ['0:00:00.110', '0:00:00.220'], 'waiting_time': ['0:00:00.400', '0:00:00.300'], 'working_time': ['0:00:42', '0:00:37'], 'exit_time': ['0:00:00.330', '0:00:00.440'], 'total_start_up_time': '0:00:00.300', 'total_init_time': '0:00:00.330', 'total_waiting_time': '0:00:00.700', 'total_working_time': '0:01:19', 'total_exit_time': '0:00:00.770', 'top_5_max_task_durations': ['0:00:06', '0:00:02', '0:00:01', '0:00:00.800', '0:00:00.100'], 'top_5_max_task_args': ['3', '2', '1', '4', '5'], 'total_time': '0:01:21.100', 'start_up_time_mean': '0:00:00.150', 'start_up_time_std': '0:00:00.050', 'init_time_mean': '0:00:00.165', 'init_time_std': '0:00:00.055', 'waiting_time_mean': '0:00:00.350', 'waiting_time_std': '0:00:00.050', 'working_time_mean': '0:00:39.500', 'working_time_std': '0:00:02.500', 'exit_time_mean': '0:00:00.385', 'exit_time_std': '0:00:00.055' }) @staticmethod def _get_tasks(n): """ Simulate that getting tasks takes some time """ for i in range(n): # sleep is added for Windows compatibility, otherwise it says 0.0 time has passed sleep(0.001) yield i @staticmethod def _init(*_): # sleep is added for Windows compatibility, otherwise it says 0.0 time has passed sleep(0.001) @staticmethod def _exit(*_): # sleep is added for Windows compatibility, otherwise it says 0.0 time has passed sleep(0.001) mpire-2.10.2/tests/test_params.py000066400000000000000000000514241461637447300167730ustar00rootroot00000000000000import unittest import warnings from functools import partial from itertools import product from unittest.mock import patch import numpy as np import pytest from tqdm import TqdmKeyError from mpire import cpu_count from mpire.params import check_map_parameters, WorkerMapParams, WorkerPoolParams, get_number_of_tasks, check_number, \ check_progress_bar_options def square(idx, x): return idx, x * x class WorkerPoolParamsTest(unittest.TestCase): def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) def test_n_jobs(self): """ When n_jobs is 0 or None it should evaluate to cpu_count(), otherwise it should stay is. """ with patch('mpire.params.mp.cpu_count', return_value=4): for n_jobs, expected_njobs in [(0, 4), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (10, 10), (None, 4)]: with self.subTest(n_jobs=n_jobs): self.assertEqual(WorkerPoolParams(n_jobs, None).n_jobs, expected_njobs) def test_check_cpu_ids_valid_input(self): """ Test that when the parameters are valid, they are converted to the correct cpu ID mask """ for n_jobs, cpu_ids, expected_mask in [(None, [0], [[0]] * cpu_count()), (None, [[0, 3]], [[0, 3]] * cpu_count()), (1, [0], [[0]]), (1, [[0, 3]], [[0, 3]]), (2, [0], [[0], [0]]), (2, [0, 1], [[0], [1]]), (2, [[0, 3]], [[0, 3], [0, 3]]), (2, [[0, 1], [0, 1]], [[0, 1], [0, 1]]), (4, [0], [[0], [0], [0], [0]]), (4, [0, 1, 2, 3], [[0], [1], [2], [3]]), (4, [[0, 3]], [[0, 3], [0, 3], [0, 3], [0, 3]])]: # The test has been designed for a system with at least 4 cores. We'll skip those test cases where the CPU # IDs exceed the number of CPUs. if cpu_ids is not None and np.array(cpu_ids).max(initial=0) >= cpu_count(): continue with self.subTest(n_jobs=n_jobs, cpu_ids=cpu_ids): params = WorkerPoolParams(n_jobs=n_jobs, cpu_ids=cpu_ids) self.assertListEqual(params.cpu_ids, expected_mask) def test_check_cpu_ids_invalid_input(self): """ Test that when parameters are invalid, an error is raised """ for n_jobs, cpu_ids in product([None, 1, 2, 4], [[0, 1], [0, 1, 2, 3], [[0, 1], [0, 1]]]): if len(cpu_ids) != (n_jobs or cpu_count()): with self.subTest(n_jobs=n_jobs, cpu_ids=cpu_ids), self.assertRaises(ValueError): WorkerPoolParams(n_jobs=n_jobs, cpu_ids=cpu_ids) # Should raise when CPU IDs are out of scope with self.assertRaises(ValueError): WorkerPoolParams(n_jobs=1, cpu_ids=[-1]) with self.assertRaises(ValueError): WorkerPoolParams(n_jobs=1, cpu_ids=[cpu_count()]) class WorkerMapParamsTest(unittest.TestCase): def test_eq(self): """ Test equality """ params = WorkerMapParams(lambda x: x, None, None, None, False, None, None, None) with self.subTest('not initialized'), warnings.catch_warnings(): warnings.simplefilter("ignore") for (func, worker_init, worker_exit, worker_lifespan, progress_bar, task_timeout, worker_init_timeout, worker_exit_timeout) in product( [self._f1, self._f2], [None, self._init1, self._init2], [None, self._exit1, self._exit2], [None, 42, 1337], [False, True], [None, 30], [None, 42], [None, 37], ): self.assertNotEqual(params, WorkerMapParams(func, worker_init, worker_exit, worker_lifespan, progress_bar, task_timeout, worker_init_timeout, worker_exit_timeout)) params = WorkerMapParams(self._f1, self._init1, self._exit1, 42, True, 1, 2, 3) with self.subTest('initialized and nothing changed'): self.assertEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 42, True, 1, 2, 3)) with self.subTest('initialized and a parameter changed'), warnings.catch_warnings(): warnings.simplefilter("ignore") self.assertNotEqual(params, WorkerMapParams(self._f2, self._init1, self._exit1, 42, True, 1, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init2, self._exit1, 42, True, 1, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit2, 42, True, 1, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 1337, True, 1, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 42, False, 1, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 42, True, 2, 2, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 42, True, 1, 3, 3)) self.assertNotEqual(params, WorkerMapParams(self._f1, self._init1, self._exit1, 42, True, 1, 2, 4)) @staticmethod def _init1(): pass @staticmethod def _init2(): pass @staticmethod def _f1(_): pass @staticmethod def _f2(_): pass @staticmethod def _exit1(): pass @staticmethod def _exit2(): pass class GetNumberOfTasksTest(unittest.TestCase): def test_get_number_of_tasks(self): """ Test that the number of tasks is correctly derived """ with self.subTest('iterable_len is provided'): self.assertEqual(get_number_of_tasks([], 100), 100) with self.subTest('iterable_len is not provided, __len__ implemented'): self.assertEqual(get_number_of_tasks([1, 2, 3], None), 3) with self.subTest('iterable_len is not provided, __len__ not implemented'): self.assertIsNone(get_number_of_tasks((x for x in []), None)) class CheckNumberTest(unittest.TestCase): def test_check_number(self): """ Test that the check_number function works as expected """ with self.subTest('correct type'): check_number(1, 'var', (int, float), False) with self.subTest('wrong type'), self.assertRaises(TypeError): check_number(1, 'var', (float,), False) with self.subTest('None allowed'): check_number(None, 'var', (int, float), True) with self.subTest('None not allowed'), self.assertRaises(TypeError): check_number(None, 'var', (int, float), False) with self.subTest('min_ provided'): check_number(1, 'var', (int, float), False, 0) with self.subTest('min_ provided, but not satisfied'), self.assertRaises(ValueError): check_number(1, 'var', (int, float), False, 2) class CheckProgressBarOptions(unittest.TestCase): @pytest.mark.filterwarnings('ignore::pytest.PytestUnraisableExceptionWarning') def test_check_progress_bar_options(self): """ Check progress_bar_options parameter. Should raise when wrong parameter values are used. """ with warnings.catch_warnings(): warnings.simplefilter("ignore") defaults = {"position": 0, "dynamic_ncols": True, "mininterval": 0.1, "maxinterval": 0.5} overrides = {"total": None, "leave": True} # Should work fine. We're ignoring warnings for progress_bar_options in [{}, {"position": 0}, {"desc": "hello", "total": 100}, {"unit": "seconds", "mininterval": 0.1}]: with self.subTest(progress_bar_options=progress_bar_options): returned_progress_bar_options = check_progress_bar_options(progress_bar_options, None, None) self.assertEqual(returned_progress_bar_options, {**defaults, **progress_bar_options, **overrides}) # progress_bar_options should be a dictionary for progress_bar_options in ['hello', {8}, 3.14]: with self.subTest(progress_bar_options=progress_bar_options), self.assertRaises(TypeError): check_progress_bar_options(progress_bar_options, None, None) # When a non-existent parameter is passed, it should raise an error with self.assertRaises(TqdmKeyError): check_progress_bar_options({"non_existent_param": "hello"}, None, None) # When a parameter is passed with a wrong type, it should raise an error. Testing other parameters causes # deadlocks in other threading tests, which on their own run just fine. It's a tqdm thing which I don't # intend to investigate any further (I tried ...) for progress_bar_options in [{"position": "hello"}]: with self.subTest(progress_bar_options=progress_bar_options), self.assertRaises(TypeError): check_progress_bar_options(progress_bar_options, None, None) # The total and leave options should be overwritten for total, leave in [(1, False), (100, True)]: returned_progress_bar_options = check_progress_bar_options({"total": 3, "leave": leave}, total, None) self.assertEqual(returned_progress_bar_options["total"], total) self.assertTrue(returned_progress_bar_options["leave"]) # Some parameters have a default value for param, value in [("position", 3), ("dynamic_ncols", False), ("mininterval", 0.5), ("maxinterval", 0.1)]: returned_progress_bar_options = check_progress_bar_options({param: value}, None, None) self.assertEqual(returned_progress_bar_options[param], value) for other_param, expected_value in [("position", 0), ("dynamic_ncols", True), ("mininterval", 0.1), ("maxinterval", 0.5)]: if param != other_param: self.assertEqual(returned_progress_bar_options[other_param], expected_value) def test_progress_bar_style(self): """ Check progress_bar_style parameter. Should raise when wrong parameter values are used. """ for progress_bar_style in [None, 'std', 'rich', 'notebook']: with self.subTest(progress_bar_style=progress_bar_style): check_progress_bar_options(None, None, progress_bar_style) for progress_bar_style in [-1, 'poor', {}]: with self.subTest(progress_bar_style=progress_bar_style), self.assertRaises(ValueError): check_progress_bar_options(None, None, progress_bar_style) class CheckMapParametersTest(unittest.TestCase): def setUp(self) -> None: # Set some defaults self.pool_params = WorkerPoolParams(3, None) self.check_map_parameters_func = partial( check_map_parameters, pool_params=self.pool_params, iterable_of_args=[], iterable_len=None, max_tasks_active=None, chunk_size=None, n_splits=None, worker_lifespan=None, progress_bar=False, progress_bar_options=None, progress_bar_style=None, task_timeout=None, worker_init_timeout=None, worker_exit_timeout=None ) def test_n_tasks(self): """ Should raise when wrong parameter values are used """ # Get number of tasks with self.subTest('get n_tasks', iterable_of_args=range(100)): n_tasks, *_ = self.check_map_parameters_func(iterable_of_args=range(100), iterable_len=None) self.assertEqual(n_tasks, 100) with self.subTest('get n_tasks, __len__ implemented', iterable_len=100): n_tasks, *_ = self.check_map_parameters_func(iterable_of_args=[1, 2, 3], iterable_len=100) self.assertEqual(n_tasks, 100) with self.subTest('get n_tasks, __len__ implemented', iterable_len=None): n_tasks, *_ = self.check_map_parameters_func(iterable_of_args=[1, 2, 3], iterable_len=None) self.assertEqual(n_tasks, 3) def test_chunk_size(self): """ When chunk_size is provided, it should be used. Otherwise, if n_splits is used and the number of tasks is known, we use chunk_size=n_tasks/n_splits. If n_splits is not provided, it is set to 4 if the number of tasks can't be determined, or to n_tasks / (n_jobs * 64) when the number of tasks is known. """ with self.subTest("check_number call"), patch('mpire.params.check_number') as p: self.check_map_parameters_func(chunk_size=10) chunk_size_call = [call for call in p.call_args_list if call[0][1] == 'chunk_size'][0] args, kwargs = chunk_size_call[0], chunk_size_call[1] self.assertEqual(args[0], 10) self.assertDictEqual(kwargs, {"allowed_types": (int, float), "none_allowed": True, "min_": 1}) with self.subTest("chunk_size provided", chunk_size=10): _, _, chunk_size, *_ = self.check_map_parameters_func(chunk_size=10) self.assertEqual(chunk_size, 10) with self.subTest("chunk_size and n_splits not provided, n_tasks provided", chunk_size=None, n_splits=None, n_tasks=11), \ patch('mpire.params.get_number_of_tasks', side_effect=[11]): _, _, chunk_size, *_ = self.check_map_parameters_func(chunk_size=None, n_splits=None) self.assertEqual(chunk_size, 11 / (3 * 64)) with self.subTest("chunk_size and n_splits not provided, n_tasks not provided", chunk_size=None, n_splits=None, n_tasks=None), \ patch('mpire.params.get_number_of_tasks', side_effect=[None]), \ warnings.catch_warnings(): warnings.simplefilter("ignore") _, _, chunk_size, *_ = self.check_map_parameters_func(chunk_size=None, n_splits=None) self.assertEqual(chunk_size, 4) with self.subTest("chunk_size not provided, n_splits provided, n_tasks not provided", chunk_size=None, n_splits=11, n_tasks=None), \ patch('mpire.params.get_number_of_tasks', side_effect=[None]), \ warnings.catch_warnings(): warnings.simplefilter("ignore") _, _, chunk_size, *_ = self.check_map_parameters_func(chunk_size=None, n_splits=None) self.assertEqual(chunk_size, 4) with self.subTest("chunk_size not provided, n_splits provided, n_tasks provided", chunk_size=None, n_splits=11, n_tasks=22), \ patch('mpire.params.get_number_of_tasks', side_effect=[22]): _, _, chunk_size, *_ = self.check_map_parameters_func(chunk_size=None, n_splits=11) self.assertEqual(chunk_size, 2) def test_n_splits(self): """ Check n_splits parameter. The actual usage of n_splits is tested in test_chunk_size """ with patch('mpire.params.check_number') as p: self.check_map_parameters_func(n_splits=11) n_splits_call = [call for call in p.call_args_list if call[0][1] == 'n_splits'][0] args, kwargs = n_splits_call[0], n_splits_call[1] self.assertEqual(args[0], 11) self.assertDictEqual(kwargs, {"allowed_types": (int,), "none_allowed": True, "min_": 1}) def test_max_tasks_active(self): """ Check max_tasks_active parameter. Should raise when wrong parameter values are used. """ with self.subTest("check_number call"), patch('mpire.params.check_number') as p: self.check_map_parameters_func(max_tasks_active=12) max_tasks_active_call = [call for call in p.call_args_list if call[0][1] == 'max_tasks_active'][0] args, kwargs = max_tasks_active_call[0], max_tasks_active_call[1] self.assertEqual(args[0], 12) self.assertDictEqual(kwargs, {"allowed_types": (int,), "none_allowed": True, "min_": 1}) # When max_active_tasks is None, it should be set to n_jobs * ceil(chunk_size) * 2 for n_jobs, chunk_size, expected_max_tasks_active in [(1, 10, 20), (2, 1.8, 8), (4, 3.14, 32)]: with self.subTest("max_active_tasks is None", n_jobs=n_jobs): pool_params = WorkerPoolParams(n_jobs, None) _, max_tasks_active, *_ = self.check_map_parameters_func(pool_params=pool_params, max_tasks_active=None, chunk_size=chunk_size) self.assertEqual(max_tasks_active, expected_max_tasks_active) def test_worker_lifespan(self): """ Check worker_lifespan parameter. Should raise when wrong parameter values are used. """ with patch('mpire.params.check_number') as p: self.check_map_parameters_func(worker_lifespan=11) worker_lifespan_call = [call for call in p.call_args_list if call[0][1] == 'worker_lifespan'][0] args, kwargs = worker_lifespan_call[0], worker_lifespan_call[1] self.assertEqual(args[0], 11) self.assertDictEqual(kwargs, {"allowed_types": (int,), "none_allowed": True, "min_": 1}) def test_timeout(self): """ Check task_timeout, worker_init_timeout, and worker_exit_timeout. Should raise when wrong parameter values are used. """ # Should work fine for timeout in [None, 0.5, 1, 100.5, int(1e8)]: with self.subTest(task_timeout=timeout), patch('mpire.params.check_number') as p: self.check_map_parameters_func(task_timeout=timeout) task_timeout_call = [call for call in p.call_args_list if call[0][1] == 'task_timeout'][0] args, kwargs = task_timeout_call[0], task_timeout_call[1] self.assertEqual(args[0], timeout) self.assertDictEqual(kwargs, {"allowed_types": (int, float), "none_allowed": True, "min_": 1e-8}) with self.subTest(worker_init_timeout=timeout), patch('mpire.params.check_number') as p: self.check_map_parameters_func(worker_init_timeout=timeout) init_timeout_call = [call for call in p.call_args_list if call[0][1] == 'worker_init_timeout'][0] args, kwargs = init_timeout_call[0], init_timeout_call[1] self.assertEqual(args[0], timeout) self.assertDictEqual(kwargs, {"allowed_types": (int, float), "none_allowed": True, "min_": 1e-8}) with self.subTest(worker_exit_timeout=timeout), patch('mpire.params.check_number') as p: self.check_map_parameters_func(worker_exit_timeout=timeout) exit_timeout_call = [call for call in p.call_args_list if call[0][1] == 'worker_exit_timeout'][0] args, kwargs = exit_timeout_call[0], exit_timeout_call[1] self.assertEqual(args[0], timeout) self.assertDictEqual(kwargs, {"allowed_types": (int, float), "none_allowed": True, "min_": 1e-8}) # timeout should be an integer, float, or None for timeout in ['3', {8}]: with self.subTest(task_timeout=timeout), self.assertRaises(TypeError): self.check_map_parameters_func(task_timeout=timeout) with self.subTest(worker_init_timeout=timeout), self.assertRaises(TypeError): self.check_map_parameters_func(worker_init_timeout=timeout) with self.subTest(worker_exit_timeout=timeout), self.assertRaises(TypeError): self.check_map_parameters_func(worker_exit_timeout=timeout) # timeout should be positive > 0 for timeout in [0, -1.337, -5]: with self.subTest(task_timeout=timeout), self.assertRaises(ValueError): self.check_map_parameters_func(task_timeout=timeout) with self.subTest(worker_init_timeout=timeout), self.assertRaises(ValueError): self.check_map_parameters_func(worker_init_timeout=timeout) with self.subTest(worker_exit_timeout=timeout), self.assertRaises(ValueError): self.check_map_parameters_func(worker_exit_timeout=timeout) mpire-2.10.2/tests/test_pool.py000066400000000000000000002350161461637447300164620ustar00rootroot00000000000000import io import os import time import types import unittest import warnings from contextlib import redirect_stderr, redirect_stdout from itertools import product, repeat from multiprocessing import Barrier, Value from threading import current_thread, main_thread, Thread from unittest.mock import Mock, patch import numpy as np from tqdm import tqdm from mpire import cpu_count, WorkerPool from mpire.async_result import AsyncResult from mpire.context import FORK_AVAILABLE, RUNNING_WINDOWS # Skip start methods that use fork if it's not available if not FORK_AVAILABLE: TEST_START_METHODS = ['spawn', 'threading'] else: TEST_START_METHODS = ['fork', 'forkserver', 'spawn', 'threading'] def square(idx, x): return idx, x * x def extremely_large_output(idx, _): return idx, os.urandom(1024 * 1024) def square_numpy(x): return x * x def subtract(x, y): return x - y class MapTest(unittest.TestCase): def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) self.test_data_len = len(self.test_data) # Numpy test data self.test_data_numpy = np.random.rand(100, 2) self.test_desired_output_numpy = square_numpy(self.test_data_numpy) self.test_data_len_numpy = len(self.test_data_numpy) def test_all_maps(self): """ Tests the map related functions """ def get_generator(iterable): yield from iterable # Test results for different parameter settings print() for n_jobs, n_tasks_max_active, worker_lifespan, chunk_size, n_splits in tqdm([ (None, None, None, None, None), (1, None, None, None, None), (2, None, None, None, None), (2, 2, None, None, None), (2, None, 2, None, None), (2, None, None, 3, None), (2, None, None, None, 3), (2, None, None, 3, 3), (2, None, 1, 3, None) ]): with WorkerPool(n_jobs=n_jobs) as pool: for map_func, sort, result_type in ((pool.map, False, list), (pool.map_unordered, True, list), (pool.imap, False, types.GeneratorType), (pool.imap_unordered, True, types.GeneratorType)): with self.subTest(map_func=map_func, input='list', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): # Test if parallel map results in the same as ordinary map function. Should work both for # generators and iterators. Also check if an empty list and extremely large output (exceeding # os.pipe limits) works as desired. results_list = map_func(square, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results_list, result_type) self.assertEqual(self.test_desired_output, sorted(results_list, key=lambda tup: tup[0]) if sort else list(results_list)) with self.subTest(map_func=map_func, input='generator', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results_list = map_func(square, get_generator(self.test_data), iterable_len=self.test_data_len, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results_list, result_type) self.assertEqual(self.test_desired_output, sorted(results_list, key=lambda tup: tup[0]) if sort else list(results_list)) with self.subTest(map_func=map_func, input='empty list', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results_list = map_func(square, [], max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results_list, result_type) self.assertEqual([], list(results_list)) # When the os pipe capacity is exceeded, a worker restart based on worker lifespan would hang if we # not fetch all the results from a worker. We only verify the amount of data returned here. with self.subTest(map_func=map_func, output='data exceeding pipe limits', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results_list = map_func(extremely_large_output, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results_list, result_type) self.assertEqual(len(self.test_desired_output), len(list(results_list))) def test_numpy_input(self): """ Test map with numpy input """ print() for n_jobs, n_tasks_max_active, worker_lifespan, chunk_size, n_splits in tqdm([ (None, None, None, None, None), (1, None, None, None, None), (2, None, None, None, None), (2, 2, None, None, None), (2, None, 2, None, None), (2, None, None, 3, None), (2, None, None, None, 3), (2, None, None, 3, 3), (2, None, 1, 3, None) ]): with WorkerPool(n_jobs=n_jobs) as pool: # Test numpy input. map should concatenate chunks of numpy output to a single output array if we # instruct it to with self.subTest(concatenate_numpy_output=True, map_function='map', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results = pool.map(square_numpy, self.test_data_numpy, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, concatenate_numpy_output=True) self.assertIsInstance(results, np.ndarray) np.testing.assert_array_equal(results, self.test_desired_output_numpy) # If we disable it we should get back chunks of the original array with self.subTest(concatenate_numpy_output=False, map_function='map', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results = pool.map(square_numpy, self.test_data_numpy, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, concatenate_numpy_output=False) self.assertIsInstance(results, list) np.testing.assert_array_equal(np.concatenate(results), self.test_desired_output_numpy) # Numpy concatenation doesn't exist for the other functions with self.subTest(map_function='imap', n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results = pool.imap(square_numpy, self.test_data_numpy, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results, types.GeneratorType) np.testing.assert_array_equal(np.concatenate(list(results)), self.test_desired_output_numpy) # map_unordered and imap_unordered cannot be checked for correctness as we don't know the order of the # returned results, except when n_jobs=1. In the other cases we could, however, check if all the values # (numpy rows) that are returned are present (albeit being in a different order) for map_func, result_type in ((pool.map_unordered, list), (pool.imap_unordered, types.GeneratorType)): with self.subTest(map_function=map_func, n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, chunk_size=chunk_size, n_splits=n_splits): results = map_func(square_numpy, self.test_data_numpy, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan) self.assertIsInstance(results, result_type) concattenated_results = np.concatenate(list(results)) if n_jobs == 1: np.testing.assert_array_equal(concattenated_results, self.test_desired_output_numpy) else: # We sort the expected and actual results using lexsort, which sorts using a sequence of # keys. We transpose the array to sort on columns instead of rows. np.testing.assert_array_equal( concattenated_results[np.lexsort(concattenated_results.T)], self.test_desired_output_numpy[np.lexsort(self.test_desired_output_numpy.T)] ) def test_dictionary_input(self): """ Test map with dictionary input """ with WorkerPool(n_jobs=1) as pool: # Should work with self.subTest('correct input'): results_list = pool.map(subtract, [{'x': 5, 'y': 2}, {'y': 5, 'x': 2}]) self.assertEqual(results_list, [3, -3]) # Should throw with self.subTest("missing 'y', unknown parameter 'z'"), self.assertRaises(TypeError): pool.map(subtract, [{'x': 5, 'z': 2}]) # Should throw with self.subTest("unknown parameter 'z'"), self.assertRaises(TypeError): pool.map(subtract, [{'x': 5, 'y': 2, 'z': 2}]) def test_start_methods(self): """ Test different start methods. All should work just fine """ print() for start_method in tqdm(TEST_START_METHODS): with self.subTest(start_method=start_method, map='map'), WorkerPool(2, start_method=start_method) as pool: results_list = pool.map(square, self.test_data) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) with self.subTest(start_method=start_method, map='map_unordered'), \ WorkerPool(2, start_method=start_method) as pool: results_list = pool.map_unordered(square, self.test_data) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, sorted(results_list, key=lambda tup: tup[0])) with self.subTest(start_method=start_method, map='imap'), WorkerPool(2, start_method=start_method) as pool: results_list = pool.imap(square, self.test_data) self.assertIsInstance(results_list, types.GeneratorType) self.assertListEqual(list(results_list), self.test_desired_output) with self.subTest(start_method=start_method, map='imap_unordered'), \ WorkerPool(2, start_method=start_method) as pool: results_list = pool.imap_unordered(square, self.test_data) self.assertIsInstance(results_list, types.GeneratorType) self.assertEqual(self.test_desired_output, sorted(results_list, key=lambda tup: tup[0])) def test_mixing_map_calls(self): """ When using the same pool, mixing map calls should raise """ with WorkerPool(2) as pool: imap_results = pool.imap(square, self.test_data) next(imap_results) # Actually start the pool with self.assertRaises(RuntimeError): pool.map(square, self.test_data) with WorkerPool(2) as pool: imap_results = pool.imap_unordered(square, self.test_data) next(imap_results) # Actually start the pool with self.assertRaises(RuntimeError): next(pool.imap(square, self.test_data)) def test_terminate(self): """ When a lazy map call is running and the pool is terminated, exhausting the results should raise """ with self.subTest("calling terminate() explicitly"), WorkerPool(1) as pool: imap_results = pool.imap(square, self.test_data) next(imap_results) # Actually start the pool pool.terminate() with self.assertRaises(RuntimeError): list(imap_results) with self.subTest("calling terminate() implicitly"): with WorkerPool(1) as pool: imap_results = pool.imap(square, self.test_data) next(imap_results) # Actually start the pool with self.assertRaises(RuntimeError): list(imap_results) # Before, this could cause a deadlock once all tests were done print() with self.subTest("calling terminate() implicitly, with progress bar"): with WorkerPool(1) as pool: imap_results = pool.imap(square, self.test_data, progress_bar=True) next(imap_results) # Actually start the pool with self.assertRaises(RuntimeError): list(imap_results) class PoolInThreadTest(unittest.TestCase): def setUp(self): self.test_data = [1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0] self.test_desired_output = [self._square(x) for x in self.test_data] def test_start_methods(self): """ Test that a WorkerPool can be started inside a thread, which isn't the main thread. Test for different start methods. All should work just fine """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method): t = Thread(target=self._map_thread, args=(start_method,)) t.start() t.join() def _map_thread(self, start_method): """ This function is called from within a thread """ self.assertNotEqual(current_thread(), main_thread()) with WorkerPool(2, start_method=start_method) as pool: results_list = pool.map(self._square, self.test_data) self.assertIsInstance(results_list, list) self.assertListEqual(self.test_desired_output, results_list) @staticmethod def _square(x): return x * x class ApplyTest(unittest.TestCase): def test_apply_async_call(self): """ Test that apply simply calls apply_async """ with WorkerPool(1) as pool, patch.object(pool, 'apply_async') as mock_apply_async: pool.apply(subtract, (1,), {'y': 2}, self._callback, self._error_callback, self._init, self._exit, 0.1, 0.2, 0.3) mock_apply_async.assert_called_once_with(subtract, (1,), {'y': 2}, self._callback, self._error_callback, self._init, self._exit, 0.1, 0.2, 0.3) def test_result(self): """ Test that apply returns the correct result """ with WorkerPool(1) as pool: results = [pool.apply(self._square, (i,)) for i in range(10)] self.assertEqual(results, [self._square(i) for i in range(10)]) @staticmethod def _callback(_): return 0 @staticmethod def _error_callback(_): return 1 @staticmethod def _init(): return @staticmethod def _exit(): return 2 @staticmethod def _square(x): return x * x class ApplyAsyncTest(unittest.TestCase): def test_result(self): """ Test that apply_async returns the correct result. Calling get multiple times should also work """ with WorkerPool(2) as pool: results = [pool.apply_async(self._square, (i,)) for i in range(10)] [self.assertIsInstance(result, AsyncResult) for result in results] self.assertListEqual([result.get() for result in results], [self._square(i) for i in range(10)]) self.assertListEqual([result.get() for result in results], [self._square(i) for i in range(10)]) def test_args_kwargs(self): """ Test that apply_async works with args and kwargs """ with WorkerPool(2) as pool: results = [pool.apply_async(subtract, (i * i,), {'y': i}) for i in range(10)] self.assertListEqual([result.get() for result in results], [subtract(i * i, i) for i in range(10)]) results = [pool.apply_async(subtract, (), {'x': i * i, 'y': i}) for i in range(10)] self.assertListEqual([result.get() for result in results], [subtract(i * i, i) for i in range(10)]) def test_callback(self): """ Test that apply_async calls the callback function on success """ callback = Mock() with WorkerPool(1) as pool: pool.apply_async(self._square, (42,), callback=callback).get() callback.assert_called_once_with(42 * 42) def test_callback_error(self): """ Test that apply_async calls the error callback function on error """ callback = Mock() with WorkerPool(1) as pool: value_error = ValueError('test') with self.assertRaises(ValueError): pool.apply_async(self._raise_exception, (value_error,), error_callback=callback).get() self.assertIsInstance(callback.call_args[0][0], ValueError) def test_second_apply_raises(self): """ When a second apply task raises an exception, the first task should still be able to complete. I.e., the second worker shouldn't cause the entire pool to shutdown """ with self.subTest("exception is raised"), WorkerPool(2) as pool: event = pool.ctx.Event() pool.set_shared_objects(event) first_result = pool.apply_async(self._wait_and_return, (42,)) with self.assertRaises(ValueError): pool.apply_async(self._raise_exception_2).get() self.assertFalse(first_result.ready()) event.set() self.assertEqual(first_result.get(), 42) with self.subTest("timeout is raised"), WorkerPool(2) as pool: event = pool.ctx.Event() pool.set_shared_objects(event) first_result = pool.apply_async(self._wait_and_return, (42,)) with self.assertRaises(TimeoutError): pool.apply_async(self._wait_and_return, (1337,), task_timeout=0.01).get() self.assertFalse(first_result.ready()) event.set() self.assertEqual(first_result.get(), 42) @staticmethod def _square(x): return x * x @staticmethod def _raise_exception(exception): raise exception @staticmethod def _raise_exception_2(_): raise ValueError @staticmethod def _wait_and_return(e, x): e.wait() return x class WorkerIDTest(unittest.TestCase): def test_by_config_function(self): """ Test setting passing on the worker ID using the pass_on_worker_id function """ for n_jobs, pass_worker_id in product([1, 3], [True, False]): with self.subTest(n_jobs=n_jobs, pass_worker_id=pass_worker_id, config_type='function'), \ WorkerPool(n_jobs=n_jobs) as pool: pool.pass_on_worker_id(pass_worker_id) # Tests should fail when number of arguments in function is incorrect, worker ID is not within range, # or when the shared objects are not equal to the given arguments f = self._f1 if pass_worker_id else self._f2 self.assertListEqual(pool.map(f, ((n_jobs,) for _ in range(10)), iterable_len=10), [True] * 10) def test_by_constructor(self): """ Test setting passing on the worker ID in the constructor """ for n_jobs, pass_worker_id in product([1, 3], [True, False]): with self.subTest(n_jobs=n_jobs, pass_worker_id=pass_worker_id, config_type='constructor'), \ WorkerPool(n_jobs=n_jobs, pass_worker_id=pass_worker_id) as pool: # Tests should fail when number of arguments in function is incorrect, worker ID is not within range, # or when the shared objects are not equal to the given arguments f = self._f1 if pass_worker_id else self._f2 self.assertListEqual(pool.map(f, ((n_jobs,) for _ in range(10)), iterable_len=10), [True] * 10) def test_start_methods(self): """ Test for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, pass_worker_id=True, start_method=start_method) as pool: self.assertListEqual(pool.map(self._f1, ((2,) for _ in range(10)), iterable_len=10), [True] * 10) @staticmethod def _f1(_wid, _n_jobs): """ Function with worker ID """ tests_succeed = True tests_succeed &= isinstance(_wid, int) tests_succeed &= _wid >= 0 tests_succeed &= _wid <= _n_jobs return tests_succeed @staticmethod def _f2(_n_jobs): """ Function without worker ID (simply tests if WorkerPool correctly handles pass_worker_id=False) """ return True class SharedObjectsTest(unittest.TestCase): def test_by_config_function(self): """ Tests passing shared objects using the set_shared_objects function """ for n_jobs, shared_objects in product([1, 3], [None, (37, 42), ({'1', '2', '3'})]): with self.subTest(n_jobs=n_jobs, shared_objects=shared_objects, config_type='function'), \ WorkerPool(n_jobs=n_jobs) as pool: # Configure pool pool.set_shared_objects(shared_objects) # Tests should fail when number of arguments in function is incorrect, worker ID is not within range, # or when the shared objects are not equal to the given arguments f = self._f1 if shared_objects else self._f2 self.assertListEqual(pool.map(f, ((shared_objects,) for _ in range(10)), iterable_len=10), [True] * 10) def test_by_constructor(self): """ Tests passing shared objects in the constructor """ for n_jobs, shared_objects in product([1, 3], [None, (37, 42), ({'1', '2', '3'})]): # Pass on arguments using the constructor instead with self.subTest(n_jobs=n_jobs, shared_objects=shared_objects, config_type='constructor'), \ WorkerPool(n_jobs=n_jobs, shared_objects=shared_objects) as pool: # Tests should fail when number of arguments in function is incorrect, worker ID is not within range, # or when the shared objects are not equal to the given arguments f = self._f1 if shared_objects else self._f2 self.assertListEqual(pool.map(f, ((shared_objects,) for _ in range(10)), iterable_len=10), [True] * 10) def test_start_methods(self): """ Tests for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, shared_objects=({'1', '2', '3'}), start_method=start_method) as pool: self.assertListEqual(pool.map(self._f1, (({'1', '2', '3'},) for _ in range(10)), iterable_len=10), [True] * 10) @staticmethod def _f1(_sobjects, _args): """ Function with shared objects """ return _sobjects == _args @staticmethod def _f2(_args): """ Function without shared objects (simply tests if WorkerPool correctly handles shared_objects=None) """ return True class WorkerStateTest(unittest.TestCase): def test_by_config_function(self): """ Tests setting worker state using the set_use_worker_state function """ for n_jobs, use_worker_state, n_tasks in product([1, 3], [False, True], [0, 1, 150]): with self.subTest(n_jobs=n_jobs, use_worker_state=use_worker_state, n_tasks=n_tasks),\ WorkerPool(n_jobs=n_jobs, pass_worker_id=True) as pool: pool.set_use_worker_state(use_worker_state) # When use_worker_state is set, the final (worker_id, n_args) of each worker should add up to the # number of given tasks f = self._f1 if use_worker_state else self._f2 results = pool.map(f, range(n_tasks), chunk_size=2) if use_worker_state: n_processed_per_worker = [0] * n_jobs for wid, n_processed, tests_succeed in results: n_processed_per_worker[wid] = n_processed self.assertTrue(tests_succeed) self.assertEqual(sum(n_processed_per_worker), n_tasks) def test_by_constructor(self): """ Tests setting worker state in the constructor """ for n_jobs, use_worker_state, n_tasks in product([1, 3], [False, True], [0, 1, 150]): with self.subTest(n_jobs=n_jobs, use_worker_state=use_worker_state, n_tasks=n_tasks), \ WorkerPool(n_jobs=n_jobs, pass_worker_id=True, use_worker_state=use_worker_state) as pool: # When use_worker_state is set, the final (worker_id, n_args) of each worker should add up to the # number of given tasks f = self._f1 if use_worker_state else self._f2 results = pool.map(f, range(n_tasks), chunk_size=2) if use_worker_state: n_processed_per_worker = [0] * n_jobs for wid, n_processed, tests_succeed in results: n_processed_per_worker[wid] = n_processed self.assertTrue(tests_succeed) self.assertEqual(sum(n_processed_per_worker), n_tasks) def test_start_methods(self): """ Test for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, pass_worker_id=True, use_worker_state=True, start_method=start_method) as pool: results = pool.map(self._f1, range(10), chunk_size=2) n_processed_per_worker = [0, 0, 0] for wid, n_processed, tests_succeed in results: n_processed_per_worker[wid] = n_processed self.assertTrue(tests_succeed) self.assertEqual(sum(n_processed_per_worker), 10) @staticmethod def _f1(_wid, _wstate, _arg): """ Function with worker ID and worker state """ tests_succeed = True tests_succeed &= isinstance(_wstate, dict) # Worker id should always be the same _wstate.setdefault('worker_id', set()).add(_wid) tests_succeed &= _wstate['worker_id'] == {_wid} # Should contain previous args _wstate.setdefault('args', []).append(_arg) return _wid, len(_wstate['args']), tests_succeed @staticmethod def _f2(_wid, _): """ Function with worker ID (simply tests if WorkerPool correctly handles use_worker_state=False) """ pass class InitFuncTest(unittest.TestCase): def setUp(self) -> None: self.test_data = range(10) self.test_desired_output = [42, 43, 44, 45, 46, 47, 48, 49, 50, 51] def test_no_init_func(self): """ If the init func is not provided, then `worker_state['test']` should fail """ with self.assertRaises(KeyError), WorkerPool(n_jobs=4, shared_objects=(None,), use_worker_state=True) as pool: pool.map(self._f, range(10), worker_init=None) def test_init_func(self): """ Test if init func is called. If it is, then `worker_state['test']` should be available. Due to the barrier we know for sure that the init func should be called as many times as there are workers """ for n_jobs in [1, 3]: shared_objects = Barrier(n_jobs), Value('i', 0) with self.subTest(n_jobs=n_jobs), WorkerPool(n_jobs=n_jobs, shared_objects=shared_objects, use_worker_state=True) as pool: results = pool.map(self._f, self.test_data, worker_init=self._init, chunk_size=1) self.assertListEqual(results, self.test_desired_output) self.assertEqual(shared_objects[1].value, n_jobs) def test_worker_lifespan(self): """ When workers have a limited lifespan they are spawned multiple times. Each time a worker starts it should call the init function. Due to the chunk size we know for sure that the init func should be called at least once for each task. However, when all tasks have been processed the workers are terminated and we don't know exactly how many workers restarted. We only know for sure that the init func should be called between 10 and 10 + n_jobs times """ for n_jobs in [1, 3]: shared_objects = Barrier(n_jobs), Value('i', 0) with self.subTest(n_jobs=n_jobs), WorkerPool(n_jobs=n_jobs, shared_objects=shared_objects, use_worker_state=True) as pool: results = pool.map(self._f, self.test_data, worker_init=self._init, chunk_size=1, worker_lifespan=1) self.assertListEqual(results, self.test_desired_output) self.assertGreaterEqual(shared_objects[1].value, 10) self.assertLessEqual(shared_objects[1].value, 10 + n_jobs) def test_error(self): """ When an exception occurs in the init function it should properly shut down """ with self.subTest("map"), self.assertRaises(ValueError), \ WorkerPool(n_jobs=4, shared_objects=(None,), use_worker_state=True) as pool: pool.map(self._f, self.test_data, worker_init=self._init_error) with self.subTest("apply"), self.assertRaises(ValueError), \ WorkerPool(n_jobs=2, shared_objects=(None,), use_worker_state=True) as pool: pool.apply(self._f, args=(0,), worker_init=self._init_error) def test_start_methods(self): """ Test for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, use_worker_state=True, start_method=start_method) as pool: shared_objects = pool.ctx.Barrier(2), pool.ctx.Value('i', 0) pool.set_shared_objects(shared_objects) results = pool.map(self._f, self.test_data, worker_init=self._init, chunk_size=1) self.assertListEqual(results, self.test_desired_output) self.assertEqual(shared_objects[1].value, 2) @staticmethod def _init(shared_objects, worker_state): barrier, call_count = shared_objects # Only wait for the other workers the first time around (it will hang when worker_lifespan=1, otherwise) if call_count.value == 0: barrier.wait() with call_count.get_lock(): call_count.value += 1 worker_state['test'] = 42 @staticmethod def _init_error(*_): raise ValueError(":(") @staticmethod def _f(_, worker_state, x): return worker_state['test'] + x class ExitFuncTest(unittest.TestCase): def setUp(self) -> None: self.test_data = range(10) self.test_desired_output = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] def test_no_exit_func(self): """ If the exit func is not provided, then exit results shouldn't be available """ shared_objects = Barrier(4), Value('i', 0) with WorkerPool(n_jobs=4, shared_objects=shared_objects, use_worker_state=True) as pool: results = pool.map(self._f1, range(10), worker_init=self._init, worker_exit=None) self.assertListEqual(results, self.test_desired_output) self.assertListEqual(pool.get_exit_results(), []) def test_exit_func(self): """ Test if exit func is called. If it is, then exit results should be available. It should have as many elements as the number of jobs and should have the right content. """ for n_jobs in [1, 3]: shared_objects = Barrier(n_jobs), Value('i', 0) with self.subTest(n_jobs=n_jobs), WorkerPool(n_jobs=n_jobs, shared_objects=shared_objects, use_worker_state=True) as pool: results = pool.map(self._f1, self.test_data, worker_init=self._init, worker_exit=self._exit) self.assertListEqual(results, self.test_desired_output) self.assertEqual(shared_objects[1].value, n_jobs) self.assertEqual(len(pool.get_exit_results()), n_jobs) self.assertEqual(sum(pool.get_exit_results()), sum(range(10))) def test_worker_lifespan(self): """ When workers have a limited lifespan they are spawned multiple times. Each time a worker exits it should call the exit function. Due to the chunk size we know for sure that the exit func should be called at least once for each task. However, when all tasks have been processed the workers are terminated and we don't know exactly how many workers restarted. We only know for sure that the exit func should be called between 10 and 10 + n_jobs times """ for n_jobs in [1, 3]: shared_objects = Barrier(n_jobs), Value('i', 0) with self.subTest(n_jobs=n_jobs), WorkerPool(n_jobs=n_jobs, shared_objects=shared_objects, use_worker_state=True) as pool: results = pool.map(self._f1, self.test_data, worker_init=self._init, worker_exit=self._exit, chunk_size=1, worker_lifespan=1) self.assertListEqual(results, self.test_desired_output) self.assertGreaterEqual(shared_objects[1].value, 10) self.assertLessEqual(shared_objects[1].value, 10 + n_jobs) self.assertEqual(len(pool.get_exit_results()), shared_objects[1].value) self.assertEqual(sum(pool.get_exit_results()), sum(range(10))) def test_exit_func_big_payload(self): """ Multiprocessing Pipes have a maximum buffer size (depending on the system it can be anywhere between 16-1024kb). Results from the pipe need to be received from the other end, before the workers are joined. Otherwise the process can hang indefinitely. Because exit results are fetched in a different way as regular results, we test that here. We send a payload of 10_000kb. """ for n_jobs, worker_lifespan in product([1, 3], [None, 2]): with self.subTest(n_jobs=n_jobs, worker_lifespan=worker_lifespan), WorkerPool(n_jobs=n_jobs) as pool: results = pool.map(self._f2, self.test_data, worker_exit=self._exit_big_payloud, chunk_size=1, worker_lifespan=worker_lifespan) self.assertListEqual(results, self.test_desired_output) self.assertTrue(bool(pool.get_exit_results())) for exit_result in pool.get_exit_results(): self.assertEqual(len(exit_result), 10_000 * 1024) def test_error(self): """ When an exception occurs in the exit function it should properly shut down """ for worker_lifespan in [None, 2]: with self.subTest("map", worker_lifespan=worker_lifespan), self.assertRaises(ValueError), \ WorkerPool(n_jobs=4) as pool: pool.map(self._f2, range(10), worker_lifespan=worker_lifespan, worker_exit=self._exit_error) with self.subTest("apply"), self.assertRaises(ValueError), WorkerPool(n_jobs=2) as pool: pool.apply(self._f2, args=(0,), worker_exit=self._exit_error) pool.stop_and_join() def test_start_methods(self): """ Test for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, use_worker_state=True, start_method=start_method) as pool: shared_objects = pool.ctx.Barrier(2), pool.ctx.Value('i', 0) pool.set_shared_objects(shared_objects) results = pool.map(self._f1, self.test_data, worker_init=self._init, worker_exit=self._exit) self.assertListEqual(results, self.test_desired_output) self.assertEqual(shared_objects[1].value, 2) self.assertEqual(len(pool.get_exit_results()), 2) self.assertEqual(sum(pool.get_exit_results()), sum(range(10))) @staticmethod def _init(shared_objects, worker_state): barrier, call_count = shared_objects # Only wait for the other workers the first time around (it will hang when worker_lifespan=1, otherwise) if call_count.value == 0: barrier.wait() worker_state['count'] = 0 @staticmethod def _f1(_, worker_state, x): worker_state['count'] += x return x @staticmethod def _f2(x): return x @staticmethod def _exit(shared_objects, worker_state): _, call_count = shared_objects with call_count.get_lock(): call_count.value += 1 return worker_state['count'] @staticmethod def _exit_big_payloud(): return np.random.bytes(10_000 * 1024) @staticmethod def _exit_error(): raise ValueError(":'(") class DaemonTest(unittest.TestCase): # This also tests nested WorkerPools. We only test spawn here as creating processes is not thread-safe def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) def test_non_daemon_nested_workerpool(self): """ Tests nested WorkerPools when daemon==False, which should work """ with WorkerPool(n_jobs=4, daemon=False, start_method='spawn') as pool: # Obtain results using nested WorkerPools results = pool.map(self._square_daemon, ((X,) for X in repeat(self.test_data, 4)), chunk_size=1) # Each of the results should match for results_list in results: self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) def test_daemon_nested_workerpool(self): """ Tests nested WorkerPools when daemon==True, which should not work """ with self.assertRaises(AssertionError), WorkerPool(n_jobs=4, daemon=True, start_method='spawn') as pool: pool.map(self._square_daemon, ((X,) for X in repeat(self.test_data, 4)), chunk_size=1) @staticmethod def _square_daemon(x): with WorkerPool(n_jobs=2) as pool: return pool.map(square, x, chunk_size=1) class CPUPinningTest(unittest.TestCase): def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) def test_cpu_pinning(self): """ Test that when parameters are valid, nothing breaks and the pinning is actually happening """ for n_jobs, cpu_ids, expected_mask in [(None, [0], [[0]] * cpu_count()), (None, [[0, 3]], [[0, 3]] * cpu_count()), (1, [0], [[0]]), (1, [[0, 3]], [[0, 3]]), (2, [0], [[0], [0]]), (2, [0, 1], [[0], [1]]), (2, [[0, 3]], [[0, 3], [0, 3]]), (2, [[0, 1], [0, 1]], [[0, 1], [0, 1]]), (4, [0], [[0], [0], [0], [0]]), (4, [0, 1, 2, 3], [[0], [1], [2], [3]]), (4, [[0, 3]], [[0, 3], [0, 3], [0, 3], [0, 3]])]: # The test has been designed for a system with at least 4 cores. We'll skip those test cases where the CPU # IDs exceed the number of CPUs. if cpu_ids is not None and np.array(cpu_ids).max(initial=0) >= cpu_count(): continue with self.subTest(n_jobs=n_jobs, cpu_ids=cpu_ids), patch('mpire.pool.set_cpu_affinity') as p, \ WorkerPool(n_jobs=n_jobs, cpu_ids=cpu_ids) as pool: # Verify results results_list = pool.map(square, self.test_data) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) # Verify that when CPU pinning is used, it is called as many times as there are jobs and is called for # each worker process ID if cpu_ids is None: self.assertEqual(p.call_args_list, []) else: self.assertEqual(p.call_count, pool.pool_params.n_jobs) mask = [call[0][1] for call in p.call_args_list] self.assertListEqual(mask, expected_mask) def test_start_methods(self): """ Test for different start methods """ # This test will fail if there are less CPUs available than specified. if cpu_count() >= 2: n_jobs, cpu_ids, expected_mask = 2, [1, 0], [[1], [0]] else: n_jobs, cpu_ids, expected_mask = 1, [0], [[0]] for start_method in TEST_START_METHODS: if start_method == 'threading': continue with self.subTest(start_method=start_method), patch('mpire.pool.set_cpu_affinity') as p, \ WorkerPool(n_jobs=n_jobs, cpu_ids=cpu_ids, start_method=start_method) as pool: # Verify results results_list = pool.map(square, self.test_data) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) # Verify that CPU pinning is used as many times as there are jobs and is called for each worker process # ID self.assertEqual(p.call_count, pool.pool_params.n_jobs) mask = [call[0][1] for call in p.call_args_list] self.assertListEqual(mask, expected_mask) # This won't work for threading with self.assertRaises(AttributeError), WorkerPool(n_jobs=n_jobs, cpu_ids=cpu_ids, start_method='threading') as pool: pool.map(square, self.test_data) class ProgressBarTest(unittest.TestCase): """ Print statements in these tests are intentional as it will print multiple progress bars """ def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) # Numpy test data self.test_data_numpy = np.random.rand(100, 2) self.test_desired_output_numpy = square_numpy(self.test_data_numpy) self.test_data_len_numpy = len(self.test_data_numpy) # Get original tqdm lock self.original_tqdm_lock = tqdm.get_lock() def tearDown(self): # The TQDM lock is temporarily changed when using a progress bar in MPIRE, here we check if it is restored # correctly afterwards. self.assertEqual(tqdm.get_lock(), self.original_tqdm_lock) def test_valid_progress_bars_regular_input(self): """ Valid progress bars are either False/True """ print() for n_jobs, progress_bar in product([None, 1, 2], [True, False]): with self.subTest(n_jobs=n_jobs), WorkerPool(n_jobs=n_jobs) as pool: results_list = pool.map(square, self.test_data, progress_bar=progress_bar) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) def test_valid_progress_bars_numpy_input(self): """ Test with numpy, as that will change the number of tasks """ print() for n_jobs, progress_bar in product([None, 1, 2], [True, False]): # Should work just fine with self.subTest(n_jobs=n_jobs, progress_bar=progress_bar), WorkerPool(n_jobs=n_jobs) as pool: results = pool.map(square_numpy, self.test_data_numpy, progress_bar=progress_bar) self.assertIsInstance(results, np.ndarray) np.testing.assert_array_equal(results, self.test_desired_output_numpy) def test_no_input_data(self): """ Test with empty iterable (this failed before) """ print() with WorkerPool() as pool: self.assertListEqual(pool.map(square, [], progress_bar=True), []) def test_progress_bar_options(self): """ Test different progress bar options. Wrong inputs are tested in test_params """ print() for progress_bar_options in [{"unit": "km"}, {"unit": "s", "desc": "I'm a pbar!"}, {"colour": "green"}]: with self.subTest(progress_bar_options=progress_bar_options), WorkerPool(n_jobs=2) as pool: results = pool.map(square, self.test_data, progress_bar=True, progress_bar_options=progress_bar_options) self.assertIsInstance(results, list) self.assertEqual(self.test_desired_output, results) def test_start_methods(self): """ Test for different start methods """ print() for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), WorkerPool(n_jobs=2, start_method=start_method) as pool: results_list = pool.map(square, self.test_data, progress_bar=True) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) def test_progres_bar_styles(self): """ Test different progress bar styles. The std style will give updates by overwriting the previous line, so all progress update lines will be there (including the 0% from the start). The rich progress bar updates the widget, so the 0% won't be there. The notebook won't update correctly in a terminal, which is fine. This means it won't show the 100% here. Finally, the dashboard style won't give any output. """ print() for progress_bar_style, expected_outputs in [ (None, ["None", "0%", "100%", "13/13"]), ('std', ["std", "0%", "100%", "13/13"]), ('rich', ["rich", "100%", "13/13"]), ('notebook', ["notebook", "0%", "0/13"]), ('dashboard', []), ]: # Some progress bars write to stdout, others to stderr. We'll capture both. output = io.StringIO() with self.subTest(progress_bar_style=progress_bar_style), redirect_stderr(output), \ redirect_stdout(output), WorkerPool(n_jobs=2) as pool: results_list = pool.map(square, self.test_data, progress_bar=True, progress_bar_style=progress_bar_style, progress_bar_options={"desc": progress_bar_style or "None"}) self.assertIsInstance(results_list, list) self.assertEqual(self.test_desired_output, results_list) # Check outputs for expected_output in expected_outputs: self.assertIn(expected_output, output.getvalue()) if not expected_outputs: self.assertEqual(output.getvalue(), '') class KeepAliveTest(unittest.TestCase): """ In these tests we make use of a barrier. This barrier ensures that we increase the counter for each worker. If it wasn't there there's a chance that the first, say 3, workers already performed all the available tasks, while the 4th worker was still spinning up. In that case the poison pill would be inserted before the fourth worker could even start a task and therefore couldn't increase the counter value. """ def setUp(self): # Create some test data self.test_data = [1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0] self.test_desired_output_f1 = [x * 2 for x in self.test_data] self.test_desired_output_f2 = [x * 3 for x in self.test_data] def test_dont_keep_alive(self): """ When keep_alive is set to False it should restart workers between map calls. This means the counter is updated each time as well. """ for n_jobs in [1, 3]: barrier = Barrier(n_jobs) counter = Value('i', 0) shared = barrier, counter with self.subTest(n_jobs=n_jobs), \ WorkerPool(n_jobs=n_jobs, shared_objects=shared, use_worker_state=True, keep_alive=False) as pool: self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs * 2) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs * 3) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs * 4) def test_keep_alive(self): """ When keep_alive is set to True it should reuse existing workers between map calls. This means the counter is only updated the first time. """ for n_jobs in [1, 3]: barrier = Barrier(n_jobs) counter = Value('i', 0) shared = barrier, counter with self.subTest(n_jobs=n_jobs), \ WorkerPool(n_jobs=n_jobs, shared_objects=shared, use_worker_state=True, keep_alive=True) as pool: self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(list(pool.imap(self._f1, self.test_data, worker_init=self._init1)), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) def test_keep_alive_map_params_change(self): """ When keep_alive is set to True it should reuse existing workers between map calls, even when the called function, init or exit functions, or the worker lifespan changes """ for n_jobs in [1, 3]: barrier = Barrier(n_jobs) counter = Value('i', 0) shared = barrier, counter with self.subTest(n_jobs=n_jobs), warnings.catch_warnings(), \ WorkerPool(n_jobs=n_jobs, shared_objects=shared, use_worker_state=True, keep_alive=True) as pool: warnings.simplefilter('ignore') self.assertListEqual(pool.map(self._f1, self.test_data, worker_lifespan=100, worker_init=self._init1, worker_exit=self._exit1), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(list(pool.imap(self._f2, self.test_data, worker_lifespan=100, worker_init=self._init1, worker_exit=self._exit2)), self.test_desired_output_f2) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(pool.map(self._f2, self.test_data, worker_lifespan=200, worker_init=self._init2, worker_exit=self._exit1), self.test_desired_output_f2) self.assertEqual(counter.value, n_jobs) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_lifespan=100, worker_init=self._init1, worker_exit=None), self.test_desired_output_f1) self.assertEqual(counter.value, n_jobs) def test_start_methods(self): """ Test for different start methods """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method), \ WorkerPool(n_jobs=2, use_worker_state=True, keep_alive=True, start_method=start_method) as pool: barrier = pool.ctx.Barrier(2) counter = pool.ctx.Value('i', 0) pool.set_shared_objects((barrier, counter)) self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, 2) barrier.reset() self.assertListEqual(list(pool.imap(self._f1, self.test_data, worker_init=self._init1)), self.test_desired_output_f1) self.assertEqual(counter.value, 2) barrier.reset() self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1), self.test_desired_output_f1) self.assertEqual(counter.value, 2) @staticmethod def _init1(_, worker_state): worker_state['already_counted'] = False @staticmethod def _init2(_, worker_state): worker_state['already_counted'] = False worker_state[4] = 2 @staticmethod def _f1(shared, worker_state, x): """ Function that waits for all workers to spin up and increases the counter by one only once per worker, returns x * 2 """ barrier, counter = shared if not worker_state['already_counted']: with counter.get_lock(): counter.value += 1 worker_state['already_counted'] = True barrier.wait() return x * 2 @staticmethod def _f2(shared, worker_state, x): """ Function that waits for all workers to spin up and increases the counter by one only once per worker, returns x * 3 """ barrier, counter = shared if not worker_state['already_counted']: with counter.get_lock(): counter.value += 1 worker_state['already_counted'] = True barrier.wait() return x * 3 @staticmethod def _exit1(_, worker_state): return worker_state['already_counted'] @staticmethod def _exit2(_, worker_state): pass class ExceptionTest(unittest.TestCase): def setUp(self): # Create some test data. Note that the regular map reads the inputs as a list of single tuples (one argument), # whereas parallel.map sees it as a list of argument lists. Therefore we give the regular map a lambda function # which mimics the parallel.map behavior. self.test_data = list(enumerate([1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0])) self.test_desired_output = list(map(lambda _args: square(*_args), self.test_data)) self.test_data_len = len(self.test_data) # Get original tqdm lock self.original_tqdm_lock = tqdm.get_lock() def tearDown(self): # The TQDM lock is temporarily changed when using a progress bar in MPIRE, here we check if it is restored # correctly afterwards. self.assertEqual(tqdm.get_lock(), self.original_tqdm_lock) def test_exceptions(self): """ Tests if MPIRE can handle exceptions well """ # This print statement is intentional as it will print multiple progress bars print() for n_jobs, n_tasks_max_active, worker_lifespan, progress_bar in [ (1, None, None, False), (3, None, None, False), (3, 1, None, False), (3, None, 1, False), (3, None, None, True), (3, 1, None, True), (3, None, 1, True), (3, 1, 1, True) ]: print(f"========== {n_jobs}, {n_tasks_max_active}, {worker_lifespan}, {progress_bar} ==========") with WorkerPool(n_jobs=n_jobs) as pool: # Should work for map like functions print("----- square_raises, map -----") with self.subTest(n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar, function='square_raises', map='map'), \ self.assertRaises(ValueError): pool.map(self._square_raises, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar) # Should work for imap like functions print("----- square_raises, imap -----") with self.subTest(n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar, function='square_raises', map='imap'), \ self.assertRaises(ValueError): list(pool.imap_unordered(self._square_raises, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar)) # Should work for map like functions print("----- square_raises_on_idx, map -----") with self.subTest(n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar, function='square_raises_on_idx', map='map'), \ self.assertRaises(ValueError): pool.map(self._square_raises_on_idx, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar) # Should work for imap like functions print("----- square_raises_on_idx, imap -----") with self.subTest(n_jobs=n_jobs, n_tasks_max_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar, function='square_raises_on_idx', map='imap'), \ self.assertRaises(ValueError): list(pool.imap_unordered(self._square_raises_on_idx, self.test_data, max_tasks_active=n_tasks_max_active, worker_lifespan=worker_lifespan, progress_bar=progress_bar)) def test_start_methods(self): """ Test for different start methods """ print() for start_method, progress_bar in product(TEST_START_METHODS, [False, True]): print(f"========== {start_method}, {progress_bar} ==========") if RUNNING_WINDOWS and progress_bar and start_method == 'threading': print("Not yet supported on Windows") continue with self.subTest(start_method=start_method, progress_bar=progress_bar), \ WorkerPool(n_jobs=2, start_method=start_method) as pool: # Should work for map like functions print("----- square_raises, map -----") with self.subTest(function='square_raises', map='map'), self.assertRaises(ValueError): pool.map(self._square_raises, self.test_data, progress_bar=progress_bar) # Should work for imap like functions print("----- square_raises, imap -----") with self.subTest(function='square_raises', map='imap'), self.assertRaises(ValueError): list(pool.imap_unordered(self._square_raises, self.test_data, progress_bar=progress_bar)) if not progress_bar: # Should work for apply like functions print("----- square_raises, apply -----") with self.subTest(function='square_raises', func='apply'), self.assertRaises(ValueError): pool.apply(self._square_raises, self.test_data[0]) # Should work for map like functions print("----- square_raises_on_idx, map -----") with self.subTest(function='square_raises_on_idx', map='map'), self.assertRaises(ValueError): pool.map(self._square_raises_on_idx, self.test_data, progress_bar=progress_bar) # Should work for imap like functions print("----- square_raises_on_idx, imap -----") with self.subTest(function='square_raises_on_idx', map='imap'), self.assertRaises(ValueError): list(pool.imap_unordered(self._square_raises_on_idx, self.test_data, progress_bar=progress_bar)) def test_defunct_processes_exit(self): """ Tests if MPIRE correctly shuts down after process becomes defunct using exit() """ print() for n_jobs, progress_bar, worker_lifespan in [(1, False, None), (3, True, 1), (3, False, 3)]: for start_method in TEST_START_METHODS: # Progress bar on Windows + threading is not supported right now if RUNNING_WINDOWS and start_method == 'threading' and progress_bar: continue print(f"========== {start_method}, {n_jobs}, {progress_bar}, {worker_lifespan} ==========") with self.subTest(n_jobs=n_jobs, progress_bar=progress_bar, worker_lifespan=worker_lifespan, start_method=start_method), self.assertRaises(SystemExit), \ WorkerPool(n_jobs=n_jobs, start_method=start_method) as pool: pool.map(self._exit, range(100), progress_bar=progress_bar, worker_lifespan=worker_lifespan) def test_defunct_processes_kill(self): """ Tests if MPIRE correctly shuts down after one process becomes defunct using os.kill(). We kill worker 0 and to be sure it's alive we set an event object and then go in an infinite loop. The kill thread waits until the event is set and then kills the worker. The other workers are also ensured to have done something so we can test what happens during restarts """ print() for n_jobs, progress_bar, worker_lifespan in [(1, False, None), (3, True, 1), (3, False, 3)]: for start_method in TEST_START_METHODS: # Can't kill threads if start_method == 'threading': continue print(f"========== {start_method}, {n_jobs}, {progress_bar}, {worker_lifespan} ==========") with self.subTest(n_jobs=n_jobs, progress_bar=progress_bar, worker_lifespan=worker_lifespan, start_method=start_method), self.assertRaises(RuntimeError), \ WorkerPool(n_jobs=n_jobs, pass_worker_id=True, start_method=start_method) as pool: events = [pool.ctx.Event() for _ in range(n_jobs)] kill_thread = Thread(target=self._kill_process, args=(events[0], pool)) kill_thread.start() pool.set_shared_objects(events) pool.map(self._worker_0_sleeps_others_square, range(100), progress_bar=progress_bar, worker_lifespan=worker_lifespan, chunk_size=1) def test_dill_deadlock(self): """ Exceptions on the queue need to be flushed before the worker is terminated. This is one example where it used to cause a deadlock (https://github.com/Slimmer-AI/mpire/issues/56) """ data = [(x, y, z) for x, y, z in zip(range(0, 100), range(42, 142), range(10, -90, -1))] with self.assertRaises(ZeroDivisionError), WorkerPool(n_jobs=5, use_dill=True) as pool: for _ in pool.imap(lambda x, y, z: x * y / z, data): pass @staticmethod def _square_raises(_, x): raise ValueError(x) @staticmethod def _square_raises_on_idx(idx, x): if idx == 5: raise ValueError(x) else: return idx, x * x @staticmethod def _exit(_): exit() @staticmethod def _worker_0_sleeps_others_square(worker_id, events, x): """ Worker 0 waits until the other workers have at least spun up and then sets her event and sleeps """ if worker_id == 0: [event.wait() for event in events[1:]] events[0].set() while True: pass else: events[worker_id].set() return x * x @staticmethod def _kill_process(event, pool): """ Wait for event and kill """ event.wait() pool._workers[0].terminate() class TimeoutTest(unittest.TestCase): def setUp(self): # Create some test data self.test_data = [1, 2, 3] def test_worker_init_timeout(self): """ Checks if the worker_init timeout is properly triggered """ print() for start_method in TEST_START_METHODS: print(f"========== {start_method}, well below timeout ==========") with self.subTest('Well below timeout', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool: self.assertListEqual(pool.map(self._f1, self.test_data, worker_init=self._init1, worker_init_timeout=100), self.test_data) print(f"========== {start_method}, exceeding timeout, map ==========") with self.subTest('Exceeding timeout, map', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.map(self._f1, self.test_data, worker_init=self._init2, worker_init_timeout=0.01) print(f"========== {start_method}, exceeding timeout, imap ==========") with self.subTest('Exceeding timeout, imap', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): for _ in pool.imap(self._f1, self.test_data, worker_init=self._init2, worker_init_timeout=0.01): pass print(f"========== {start_method}, exceeding timeout, apply ==========") with self.subTest('Exceeding timeout, apply', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.apply(self._f1, self.test_data[0], worker_init=self._init2, worker_init_timeout=0.01) def test_worker_task_timeout(self): """ Checks if the worker_init timeout is properly triggered """ print() for start_method in TEST_START_METHODS: print(f"========== {start_method}, well below timeout ==========") with self.subTest('Well below timeout', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool: self.assertListEqual(pool.map(self._f1, self.test_data, task_timeout=100), self.test_data) print(f"========== {start_method}, exceeding timeout, map ==========") with self.subTest('Exceeding timeout, map', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.map(self._f2, self.test_data, task_timeout=0.01) print(f"========== {start_method}, exceeding timeout, imap ==========") with self.subTest('Exceeding timeout, imap', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): for _ in pool.imap(self._f2, self.test_data, task_timeout=0.01): pass print(f"========== {start_method}, exceeding timeout, apply ==========") with self.subTest('Exceeding timeout, apply', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.apply(self._f2, self.test_data[0], task_timeout=0.01) def test_worker_exit_timeout(self): """ Checks if the worker_exit timeout is properly triggered """ print() for start_method in TEST_START_METHODS: print(f"========== {start_method}, well below timeout ==========") with self.subTest('Well below timeout', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool: self.assertListEqual(pool.map(self._f1, self.test_data, worker_exit=self._exit1, worker_exit_timeout=100), self.test_data) print(f"========== {start_method}, exceeding timeout, map ==========") with self.subTest('Exceeding timeout, map', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.map(self._f1, self.test_data, worker_exit=self._exit2, worker_exit_timeout=0.01) print(f"========== {start_method}, exceeding timeout, imap ==========") with self.subTest('Exceeding timeout, imap', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): for _ in pool.imap(self._f1, self.test_data, worker_exit=self._exit2, worker_exit_timeout=0.01): pass print(f"========== {start_method}, exceeding timeout, apply ==========") with self.subTest('Exceeding timeout, apply', start_method=start_method), \ WorkerPool(2, start_method=start_method) as pool, self.assertRaises(TimeoutError): pool.apply(self._f1, self.test_data[0], worker_exit=self._exit2, worker_exit_timeout=0.01) pool.stop_and_join() def test_apply_async_multiple_task_timeout(self): """ Test that some apply_async() tasks time out correctly and don't kill the whole pool """ print() for start_method in tqdm(TEST_START_METHODS): with WorkerPool(n_jobs=3, start_method=start_method) as pool: results = [pool.apply_async(self._f3, (i,), task_timeout=0.1) for i in range(6)] for i, result in enumerate(results): if i % 2 == 0: self.assertEqual(result.get(), i) else: with self.assertRaises(TimeoutError): result.get() @staticmethod def _init1(): pass @staticmethod def _init2(): time.sleep(1) @staticmethod def _f1(x): return x @staticmethod def _f2(x): time.sleep(1) return x @staticmethod def _exit1(): pass @staticmethod def _exit2(): time.sleep(1) @staticmethod def _f3(x): if x % 2 == 0: return x else: time.sleep(1) return x class OrderTasksTest(unittest.TestCase): """ Tests if the tasks are properly ordered """ def setUp(self): # Create some test data self.test_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] def test_order_tasks(self): """ Checks if the tasks are properly ordered """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method, chunk_size=1), \ WorkerPool(4, start_method=start_method, pass_worker_id=True, use_worker_state=True, order_tasks=True) as pool: pool.map_unordered(self._f, self.test_data, worker_init=self._init, worker_exit=self._exit, chunk_size=1) exit_results = sorted(pool.get_exit_results(), key=lambda state: state['worker_id']) self.assertListEqual(exit_results, [{'worker_id': 0, 'tasks': [1, 5, 9, 13, 17]}, {'worker_id': 1, 'tasks': [2, 6, 10, 14, 18]}, {'worker_id': 2, 'tasks': [3, 7, 11, 15, 19]}, {'worker_id': 3, 'tasks': [4, 8, 12, 16, 20]}]) with self.subTest(start_method=start_method, chunk_size=3), \ WorkerPool(4, start_method=start_method, pass_worker_id=True, use_worker_state=True, order_tasks=True) as pool: pool.map_unordered(self._f, self.test_data, worker_init=self._init, worker_exit=self._exit, chunk_size=3) exit_results = sorted(pool.get_exit_results(), key=lambda state: state['worker_id']) self.assertListEqual(exit_results, [{'worker_id': 0, 'tasks': [1, 2, 3, 13, 14, 15]}, {'worker_id': 1, 'tasks': [4, 5, 6, 16, 17, 18]}, {'worker_id': 2, 'tasks': [7, 8, 9, 19, 20]}, {'worker_id': 3, 'tasks': [10, 11, 12]}]) def test_order_tasks_twice(self): """ Checks if the tasks are properly ordered the second time around as well. """ for start_method in TEST_START_METHODS: with self.subTest(start_method=start_method, chunk_size=3, keep_alive=True), \ WorkerPool(4, start_method=start_method, pass_worker_id=True, use_worker_state=True, order_tasks=True, keep_alive=True) as pool: pool.map_unordered(self._f, self.test_data, worker_init=self._init, worker_exit=self._exit, chunk_size=3) pool.map_unordered(self._f, self.test_data, worker_init=self._init, worker_exit=self._exit, chunk_size=3) pool.stop_and_join() exit_results = sorted(pool.get_exit_results(), key=lambda state: state['worker_id']) self.assertListEqual(exit_results, [{'worker_id': 0, 'tasks': [1, 2, 3, 13, 14, 15, 1, 2, 3, 13, 14, 15]}, {'worker_id': 1, 'tasks': [4, 5, 6, 16, 17, 18, 4, 5, 6, 16, 17, 18]}, {'worker_id': 2, 'tasks': [7, 8, 9, 19, 20, 7, 8, 9, 19, 20]}, {'worker_id': 3, 'tasks': [10, 11, 12, 10, 11, 12]}]) @staticmethod def _init(wid, state): state['worker_id'] = wid state['tasks'] = [] @staticmethod def _f(_, state, x): state['tasks'].append(x) @staticmethod def _exit(_, state): return state mpire-2.10.2/tests/test_signal.py000066400000000000000000000056411461637447300167650ustar00rootroot00000000000000import multiprocessing as mp import os import signal import unittest from mpire.context import RUNNING_WINDOWS from mpire.signal import DelayedKeyboardInterrupt, DisableKeyboardInterruptSignal from tests.utils import ConditionalDecorator @ConditionalDecorator(unittest.skip("Signals aren't fully supported on Windows"), RUNNING_WINDOWS) class DelayedKeyboardInterruptTest(unittest.TestCase): def test_delayed_keyboard_interrupt(self): """ The process should delay the keyboard interrupt in case ``in_thread=False``, so the expected value should be 1. However, we can't send signals to threads and so the DelayedKeyboardInterrupt doesn't do anything in that case. So there's no point in testing this with threading """ # Create events so we know when the process has started and we can send an interrupt started_event = mp.Event() quit_event = mp.Event() value = mp.Value('i', 0) # Start process and wait until it starts p = mp.Process(target=self.delayed_process_job, args=(started_event, quit_event, value)) p.start() started_event.wait() # Send kill signal and wait for it to join os.kill(p.pid, signal.SIGINT) quit_event.set() p.join() # Verify expected value. self.assertEqual(value.value, 1) @staticmethod def delayed_process_job(started_event: mp.Event, quit_event: mp.Event, value: mp.Value): """ Should be affected by interrupt """ try: with DelayedKeyboardInterrupt(): started_event.set() quit_event.wait() value.value = 1 except KeyboardInterrupt: pass else: value.value = 2 @ConditionalDecorator(unittest.skip("Signals aren't fully supported on Windows"), RUNNING_WINDOWS) class DisabledKeyboardInterruptTest(unittest.TestCase): def test_disabled_keyboard_interrupt(self): """ The process should ignore a keyboard interrupt entirely, which means the expected value should be True """ # Create events so we know when the process has started and we can send an interrupt started_event = mp.Event() quit_event = mp.Event() value = mp.Value('b', False) p = mp.Process(target=self.disabled_process_job, args=(started_event, quit_event, value)) p.start() started_event.wait() os.kill(p.pid, signal.SIGINT) quit_event.set() p.join() # If everything worked the value should be set to True self.assertEqual(value.value, True) @staticmethod def disabled_process_job(started_event: mp.Event, quit_event: mp.Event, value: mp.Value): """ Should not be affected by interrupt """ with DisableKeyboardInterruptSignal(): started_event.set() quit_event.wait() value.value = True mpire-2.10.2/tests/test_utils.py000066400000000000000000000560301461637447300166460ustar00rootroot00000000000000import types import unittest from itertools import chain, product from multiprocessing import cpu_count from unittest.mock import patch import numpy as np from mpire.utils import apply_numpy_chunking, chunk_tasks, format_seconds, get_n_chunks, make_single_arguments, TimeIt class ChunkTasksTest(unittest.TestCase): def test_no_chunk_size_no_n_splits_provided(self): """ Test that a ValueError is raised when no chunk_size and n_splits are provided """ with self.assertRaises(ValueError): next(chunk_tasks([])) def test_generator_without_iterable_len(self): """ Test that a ValueError is raised when a generator is provided without iterable_len """ with self.assertRaises(ValueError): next(chunk_tasks(iter([]), n_splits=1)) def test_chunk_size_has_priority_over_n_splits(self): """ Test that chunk_size is prioritized over n_splits """ chunks = list(chunk_tasks(range(4), chunk_size=4, n_splits=4)) self.assertEqual(len(chunks), 1) self.assertEqual(len(chunks[0]), 4) self.assertEqual(list(range(4)), list(chain.from_iterable(chunks))) def test_empty_input(self): """ Test that the chunker is an empty generator for an empty input iterable """ with self.subTest('list input'): chunks = list(chunk_tasks([], n_splits=5)) self.assertEqual(len(chunks), 0) with self.subTest('generator/iterator input'): chunks = list(chunk_tasks(iter([]), iterable_len=0, n_splits=5)) self.assertEqual(len(chunks), 0) def test_iterable_len_doesnt_match_input_size(self): """ Test for cases where iterable_len does and does not match the number of arguments (it should work fine) """ num_args = 10 for iter_len in [5, 10, 20]: expected_args_sum = min(iter_len, num_args) # Test for normal list (range is considered a normal list as it implements __len__ and such) with self.subTest(iter_len=iter_len, input='list'): chunks = list(chunk_tasks(range(num_args), iterable_len=iter_len, n_splits=1)) total_args = sum(map(len, chunks)) self.assertEqual(total_args, expected_args_sum) self.assertEqual(list(range(expected_args_sum)), list(chain.from_iterable(chunks))) # Test for an actual generator (range does not really behave like one) with self.subTest(iter_len=iter_len, input='generator/iterator'): chunks = list(chunk_tasks(iter(range(num_args)), iterable_len=iter_len, n_splits=1)) total_args = sum(map(len, chunks)) self.assertEqual(total_args, expected_args_sum) self.assertEqual(list(range(expected_args_sum)), list(chain.from_iterable(chunks))) def test_n_splits(self): """ Test different values of n_splits: len(args) {<, ==, >} n_splits """ n_splits = 5 for num_args in [n_splits - 1, n_splits, n_splits + 1]: expected_n_chunks = min(n_splits, num_args) # Test for normal list (range is considered a normal list as it implements __len__ and such) with self.subTest(num_args=num_args, input='list'): chunks = list(chunk_tasks(range(num_args), n_splits=n_splits)) self.assertEqual(len(chunks), expected_n_chunks) self.assertEqual(list(range(num_args)), list(chain.from_iterable(chunks))) # Test for an actual generator (range does not really behave like one) with self.subTest(num_args=num_args, input='generator/iterator'): chunks = list(chunk_tasks(iter(range(num_args)), iterable_len=num_args, n_splits=n_splits)) self.assertEqual(len(chunks), expected_n_chunks) self.assertEqual(list(range(num_args)), list(chain.from_iterable(chunks))) def test_chunk_size(self): """ Test that chunks are of the right size if chunk_size is provided """ chunk_size = 3 for num_args in [chunk_size - 1, chunk_size, chunk_size + 1]: # Test for normal list (range is considered a normal list as it implements __len__ and such) with self.subTest(num_args=num_args, input='list'): chunks = list(chunk_tasks(range(num_args), chunk_size=chunk_size)) for chunk in chunks[:-1]: self.assertEqual(len(chunk), chunk_size) self.assertLessEqual(len(chunks[-1]), chunk_size) self.assertEqual(list(range(num_args)), list(chain.from_iterable(chunks))) # Test for an actual generator (range does not really behave like one) with self.subTest(num_args=num_args, input='generator/iterator'): chunks = list(chunk_tasks(iter(range(num_args)), chunk_size=chunk_size)) for chunk in chunks[:-1]: self.assertEqual(len(chunk), chunk_size) self.assertLessEqual(len(chunks[-1]), chunk_size) self.assertEqual(list(range(num_args)), list(chain.from_iterable(chunks))) class ApplyNumpyChunkingTest(unittest.TestCase): """ This function simply calls other, already tested, functions in succession. We do test the individual parameter influence, but interactions between them are skipped """ def setUp(self): self.test_data_numpy = np.random.rand(100, 2) def test_iterable_len(self): """ Test that iterable_len is adhered to. When iterable_len < len(input) it should reduce the input size. If higher or None it should take the entire input """ for iterable_len, expected_size in [(5, 5), (150, 100), (None, 100)]: with self.subTest(iterable_len=iterable_len): iterable_of_args, iterable_len_, chunk_size, n_splits = apply_numpy_chunking( self.test_data_numpy, iterable_len=iterable_len, n_splits=1 ) # Materialize generator and test contents iterable_of_args = list(iterable_of_args) self.assertEqual(len(iterable_of_args), 1) self.assertIsInstance(iterable_of_args[0][0], np.ndarray) np.testing.assert_array_equal(iterable_of_args[0][0], self.test_data_numpy[:expected_size]) # Test other output self.assertEqual(iterable_len_, 1) self.assertEqual(chunk_size, 1) self.assertIsNone(n_splits) def test_chunk_size(self): """ Test that chunk_size works as expected. Note that chunk_size trumps n_splits """ for chunk_size, expected_n_chunks in [(1, 100), (3, 34), (200, 1), (None, 1)]: with self.subTest(chunk_size=chunk_size): iterable_of_args, iterable_len, chunk_size_, n_splits = apply_numpy_chunking( self.test_data_numpy, chunk_size=chunk_size, n_splits=1 ) # Materialize generator and test contents. The chunks should be of size chunk_size (expect for the last # chunk which can be smaller) iterable_of_args = list(iterable_of_args) self.assertEqual(len(iterable_of_args), expected_n_chunks) chunk_size = chunk_size or 100 for chunk_idx, chunk in enumerate(iterable_of_args): self.assertIsInstance(chunk[0], np.ndarray) np.testing.assert_array_equal(chunk[0], self.test_data_numpy[chunk_idx * chunk_size: (chunk_idx + 1) * chunk_size]) # Test other output self.assertEqual(iterable_len, expected_n_chunks) self.assertEqual(chunk_size_, 1) self.assertIsNone(n_splits) def test_n_splits(self): """ Test that n_splits works as expected. """ for n_splits, expected_n_chunks in [(1, 1), (3, 3), (150, 100)]: with self.subTest(n_splits=n_splits): iterable_of_args, iterable_len, chunk_size, n_splits_ = apply_numpy_chunking( self.test_data_numpy, n_splits=n_splits ) # Materialize generator and test contents. We simply test if every row of the original input occurs in # the chunks iterable_of_args = list(iterable_of_args) self.assertEqual(len(iterable_of_args), expected_n_chunks) offset = 0 for chunk in iterable_of_args: self.assertIsInstance(chunk[0], np.ndarray) np.testing.assert_array_equal(chunk[0], self.test_data_numpy[offset:offset + len(chunk[0])]) offset += len(chunk[0]) self.assertEqual(offset, 100) # Test other output self.assertEqual(iterable_len, expected_n_chunks) self.assertEqual(chunk_size, 1) self.assertIsNone(n_splits_) # chunk_size and n_splits can't be both None with self.subTest(n_splits=None), self.assertRaises(ValueError): iterable_of_args, *_ = apply_numpy_chunking(self.test_data_numpy, n_splits=None) list(iterable_of_args) def test_n_jobs(self): """ Test that n_jobs works as expected. When chunk_size and n_splits are both None, n_jobs * 4 is passed on as n_splits """ for n_jobs, expected_n_chunks in [(1, 4), (3, 12), (40, 100), (150, 100)]: with self.subTest(n_jobs=n_jobs): iterable_of_args, iterable_len, chunk_size, n_splits_ = apply_numpy_chunking( self.test_data_numpy, n_jobs=n_jobs ) # Materialize generator and test contents. We simply test if every row of the original input occurs in # the chunks iterable_of_args = list(iterable_of_args) self.assertEqual(len(iterable_of_args), expected_n_chunks) offset = 0 for chunk in iterable_of_args: self.assertIsInstance(chunk[0], np.ndarray) np.testing.assert_array_equal(chunk[0], self.test_data_numpy[offset:offset + len(chunk[0])]) offset += len(chunk[0]) self.assertEqual(offset, 100) # Test other output self.assertEqual(iterable_len, expected_n_chunks) self.assertEqual(chunk_size, 1) self.assertIsNone(n_splits_) class GetNChunksTest(unittest.TestCase): def setUp(self): self.test_data = [1, 2, 3, 5, 6, 9, 37, 42, 1337, 0, 3, 5, 0] self.test_data_numpy = np.random.rand(100, 2) def test_everything_none(self): """ When everything is None we should use cpu_count * 4 as number of splits. We have to take the number of tasks into account """ with self.subTest(input='list'): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=None, n_splits=None, n_jobs=None), min(13, cpu_count() * 4)) with self.subTest(input='numpy'): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=None, n_splits=None, n_jobs=None), min(100, cpu_count() * 4)) def test_smaller_iterable_len(self): """ Test iterable_len, where iterable_len < len(input) """ with self.subTest(input='list'): self.assertEqual(get_n_chunks(self.test_data, iterable_len=5, chunk_size=None, n_splits=None, n_jobs=None), min(5, cpu_count() * 4)) with self.subTest(input='numpy'): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=5, chunk_size=None, n_splits=None, n_jobs=None), min(5, cpu_count() * 4)) with self.subTest(input='generator/iterator'): self.assertEqual(get_n_chunks(iter(self.test_data), iterable_len=5, chunk_size=None, n_splits=None, n_jobs=None), min(5, cpu_count() * 4)) def test_larger_iterable_len(self): """ Test iterable_len, where iterable_len > len(input). Should ignores iterable_len when actual number of tasks is less, except when we use the data_generator function, in which case we cannot determine the actual number of elements. """ with self.subTest(input='list'): self.assertEqual(get_n_chunks(self.test_data, iterable_len=25, chunk_size=None, n_splits=None, n_jobs=None), min(13, cpu_count() * 4)) with self.subTest(input='numpy'): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=125, chunk_size=None, n_splits=None, n_jobs=None), min(100, cpu_count() * 4)) with self.subTest(input='generator/iterator'): self.assertEqual(get_n_chunks(iter(self.test_data), iterable_len=25, chunk_size=None, n_splits=None, n_jobs=None), min(25, cpu_count() * 4)) def test_chunk_size(self): """ Test chunk_size """ for chunk_size, expected_n_chunks in [(1, 13), (3, 5)]: with self.subTest(input='list', chunk_size=chunk_size, expected_n_chunks=expected_n_chunks): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=chunk_size, n_splits=None, n_jobs=None), expected_n_chunks) for chunk_size, expected_n_chunks in [(1, 100), (3, 34)]: with self.subTest(input='list', chunk_size=chunk_size, expected_n_chunks=expected_n_chunks): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=chunk_size, n_splits=None, n_jobs=None), expected_n_chunks) def test_n_splits(self): """ Test n_splits. n_jobs shouldn't have any influence """ for n_splits, n_jobs in product([1, 6], [None, 2, 8]): with self.subTest(input='list', n_splits=n_splits, n_jobs=n_jobs): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=None, n_splits=n_splits, n_jobs=n_jobs), n_splits) with self.subTest(input='numpy', n_splits=n_splits, n_jobs=n_jobs): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=None, n_splits=n_splits, n_jobs=n_jobs), n_splits) def test_n_jobs(self): """ When everything is None except n_jobs we should use n_jobs * 4 as number of splits. Again, taking into account the number of tasks """ for n_jobs in [1, 6]: with self.subTest(input='list', n_jobs=n_jobs): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=None, n_splits=None, n_jobs=n_jobs), min(4 * n_jobs, len(self.test_data))) with self.subTest(input='numpy', n_jobs=n_jobs): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=None, n_splits=None, n_jobs=n_jobs), min(4 * n_jobs, len(self.test_data_numpy))) def test_chunk_size_priority_over_n_splits(self): """ chunk_size should have priority over n_splits """ with self.subTest(input='list', chunk_size=1, n_splits=6): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=1, n_splits=6, n_jobs=None), 13) with self.subTest(input='numpy', chunk_size=1, n_splits=6): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=1, n_splits=6, n_jobs=None), 100) with self.subTest(input='list', chunk_size=3, n_splits=3): self.assertEqual(get_n_chunks(self.test_data, iterable_len=None, chunk_size=3, n_splits=3, n_jobs=None), 5) with self.subTest(input='numpy', chunk_size=3, n_splits=3): self.assertEqual(get_n_chunks(self.test_data_numpy, iterable_len=None, chunk_size=3, n_splits=3, n_jobs=None), 34) def test_generator_input_with_no_iterable_len_raises(self): """ When working with generators the iterable_len should be provided (the working examples are already tested above) """ for chunk_size, n_splits, n_jobs in product([None, 1, 3], [None, 1, 3], [None, 1, 3]): with self.subTest(chunk_size=chunk_size, n_splits=n_splits, n_jobs=n_jobs), self.assertRaises(ValueError): get_n_chunks(iter(self.test_data), iterable_len=None, chunk_size=chunk_size, n_splits=n_splits, n_jobs=n_jobs) class MakeSingleArgumentsTest(unittest.TestCase): def test_make_single_arguments(self): """ Tests the make_single_arguments function for different inputs """ # Test for some different inputs for (args_in, args_out), generator in product( [(['a', 'c', 'b', 'd'], [('a',), ('c',), ('b',), ('d',)]), ([1, 2, 3, 4, 5], [(1,), (2,), (3,), (4,), (5,)]), ([(True,), (False,), (None,)], [((True,),), ((False,),), ((None,),)])], [False, True] ): # Transform args_transformed = make_single_arguments((arg for arg in args_in) if generator else args_in, generator=generator) # Check type self.assertTrue(isinstance(args_transformed, types.GeneratorType if generator else list)) # Check contents self.assertEqual(list(args_transformed), args_out) class FormatSecondsTest(unittest.TestCase): def test_none_input(self): """ When the input is None it should return an empty string """ for with_milliseconds in [False, True]: with self.subTest(with_milliseconds=with_milliseconds): self.assertEqual(format_seconds(None, with_milliseconds=with_milliseconds), '') def test_without_milliseconds(self): """ Test output without milliseconds """ for seconds, expected_output in [(0, '0:00:00'), (1, '0:00:01'), (1.337, '0:00:01'), (2.9, '0:00:02'), (123456.78901234, '1 day, 10:17:36')]: with self.subTest(seconds=seconds): self.assertEqual(format_seconds(seconds, with_milliseconds=False), expected_output) def test_with_milliseconds(self): """ Test output with milliseconds. Only shows them when they're actually needed. """ for seconds, expected_output in [(0, '0:00:00'), (1, '0:00:01'), (1.337, '0:00:01.337'), (2.9, '0:00:02.900'), (123456.78901234, '1 day, 10:17:36.789')]: with self.subTest(seconds=seconds): self.assertEqual(format_seconds(seconds, with_milliseconds=True), expected_output) class TimeItTest(unittest.TestCase): def test_array_storage(self): """ TimeIt should write to the correct idx in the cum_time_array container. The max_time_array is a min-heap container, so the lowest value is stored at index 0. The single highest value in this case is stored at index 2 """ for array_idx in range(5): cum_time_array = [0.0, 0.0, 0.0, 0.0, 0.0] max_time_array = [(0.0, ''), (0.0, ''), (0.0, ''), (0.0, ''), (0.0, '')] with self.subTest(array_idx=array_idx), patch('mpire.utils.time.time', side_effect=[0.0, 4.2]), \ TimeIt(cum_time_array, array_idx, max_time_array): pass self.assertListEqual([t for idx, t in enumerate(cum_time_array) if idx != array_idx], [0.0, 0.0, 0.0, 0.0]) self.assertListEqual([t for idx, t in enumerate(max_time_array) if idx != 2.0], [(0.0, ''), (0.0, ''), (0.0, ''), (0.0, '')]) self.assertEqual(cum_time_array[array_idx], 4.2) self.assertGreaterEqual(max_time_array[2], (4.2, None)) def test_cum_time(self): """ Using TimeIt multiple times should increase the cum_time_array """ # These return values are used by TimeIt in order: start, end, start, end, ... So the first time the duration # will be 1 second, then 2 seconds, and 3 seconds. cum_time_array = [0] with patch('mpire.utils.time.time', side_effect=[0.0, 1.0, 0.0, 2.0, 0.0, 3.0]): with TimeIt(cum_time_array, 0): pass self.assertEqual(cum_time_array[0], 1.0) with TimeIt(cum_time_array, 0): pass self.assertEqual(cum_time_array[0], 3.0) with TimeIt(cum_time_array, 0): pass self.assertEqual(cum_time_array[0], 6.0) def test_max_time(self): """ Using TimeIt multiple times should store the max duration value in the max_time_array using heapq. There's only room for the highest 5 values, while it is called 6 times. The smallest duration shouldn't be present. """ # These return values are used by TimeIt in order: start, end, start, end, ... So the first time the duration # will be 1 second, then 2 seconds, 3 seconds, 3 seconds again, 0.5 seconds, and 10 seconds. cum_time_array = [0.0] max_time_array = [(0.0, ''), (0.0, ''), (0.0, ''), (0.0, ''), (0.0, '')] with patch('mpire.utils.time.time', side_effect=[0.0, 1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 3.0, 0.0, 0.5, 0.0, 10.0]): for _ in range(6): with TimeIt(cum_time_array, 0, max_time_array): pass self.assertListEqual(max_time_array, [(1.0, None), (2.0, None), (10.0, None), (3.0, None), (3.0, None)]) def test_format_args(self): """ The format args func should be called when provided """ for format_func, formatted in [(lambda: "1", "1"), (lambda: 2, 2), (lambda: "foo", "foo")]: # These return values are used by TimeIt in order: start, end, start, end, ... So the first time the # duration will be 1 second, then 2 seconds, and 3 seconds. with self.subTest(format_func=format_func), \ patch('mpire.utils.time.time', side_effect=[0.0, 1.0, 0.0, 2.0, 0.0, 3.0]): cum_time_array = [0.0] max_time_array = [(0.0, ''), (0.0, '')] for _ in range(3): with TimeIt(cum_time_array, 0, max_time_array, format_func): pass # The heapq only had room for two entries. The highest durations should be kept self.assertListEqual(max_time_array, [(2.0, formatted), (3.0, formatted)]) mpire-2.10.2/tests/utils.py000066400000000000000000000014021461637447300156000ustar00rootroot00000000000000from typing import Callable class ConditionalDecorator: def __init__(self, decorator: Callable, condition: bool) -> None: """ Decorator which takes a decorator and a condition as input. Only when the condition is met the decorator is used :param decorator: Decorator :param condition: Condition (boolean) """ self.decorator = decorator self.condition = condition def __call__(self, func) -> Callable: """ Enables the conditional decorator :param func: Function to decorated :return: Decorated function if condition is met, otherwise just the function """ if self.condition: return self.decorator(func) else: return func