././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/0000775000174700017470000000000014603772011014431 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/AUTHORS0000664000174700017470000000107314603772010015501 0ustar00gitlab-runnergitlab-runner# This is the list of JUBE's significant contributors. # # This does not necessarily list everyone who has contributed code, # especially since many employees of one corporation may be contributing. # To see the full list of contributors, see the revision history in # source control. Forschungszentrum Jülich GmbH Alexander Trautmann Alexandre Strube Andreas Herten Andreas Klasen Carina Himmels Filipe Guimarães Jan-Oliver Mirus Julia Wellmann Kay Thust Sebastian Achilles Sebastian Lührs Thomas Breuer Wolfgang Frings Yannik Müller Universiteit Gent Andy Georges ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/CITATION.cff0000664000174700017470000000161714603772010016327 0ustar00gitlab-runnergitlab-runnercff-version: 1.2.0 title: JUBE message: 'If you use this software, please cite it as below.' type: software authors: - family-names: Breuer given-names: Thomas orcid: 'https://orcid.org/0000-0003-3979-4795' - family-names: Wellmann given-names: Julia orcid: 'https://orcid.org/0000-0001-6631-8220' - family-names: Souza Mendes Guimarães given-names: Filipe orcid: 'https://orcid.org/0000-0002-5618-6727' - family-names: Himmels given-names: Carina orcid: 'https://orcid.org/0009-0009-8095-7112' - family-names: Luehrs given-names: Sebastian orcid: 'https://orcid.org/0000-0001-8496-8630' identifiers: - type: doi value: 10.5281/zenodo.7534372 description: The concept DOI of this software repository-code: 'https://github.com/FZJ-JSC/JUBE' url: 'https://www.fz-juelich.de/jsc/jube' license: GPL-3.0-or-later contact: - email: jube.jsc@fz-juelich.de././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/CODE_OF_CONDUCT.md0000664000174700017470000001256614603772010017241 0ustar00gitlab-runnergitlab-runner # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement [here](jube.jsc@fz-juelich.de). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/CONTRIBUTING.md0000664000174700017470000001554714603772010016675 0ustar00gitlab-runnergitlab-runner# Contributing to JUBE If you are interested in doing so, you can: - [Send feedback](#send-feedback) - [Contribute to Code](#contribute-to-code) We are happy to receive them! ## Send Feedback Report any bugs or send us requests [here](https://github.com/FZJ-JSC/JUBE/issues). Please try to avoid duplications and describe your feedback as best as possible. Alternatively, you can contact the JSC JUBE developers via the following email address: [jube.jsc@fz-juelich.de](mailto:jube.jsc@fz-juelich.de) ## Contribute to Code You are welcome to contribute to JUBE's code! To do so, please fork the repository on [GitHub](https://github.com/FZJ-JSC/JUBE) and create a pull request (PR). Contributions should follow the following rules: 1. We suggest to follow some [Guidelines for Contributions](#guidelines-for-contributions) (we also try to follow them, although not always successfully) 2. The code is currently distributed under the GPLv3 License 3. You should **agree to the [Contributors License Agreement](#jube-contributor-license-agreement)** ### Guidelines for Contributions Please, try to follow these guidelines for your code contributions: - Add comments when possible - Use clean code - Use 4 spaces for indentation - Update the documentation in `docs` and make sure it is properly formatted - Update the `docs/glossar.rst` file (Don't forget to update the `general structure` section if necessary) - Update the `docs/commandline.rst` file if you have modified or added command line options - Adapt `contrib/schema/*` files and yaml converter corresponding to newly developed options - Conform the source code to pep8, for example with `autopep8` - Add new examples within `examples`, if there are new features - Extend `docs/release_notes.rst` with small sentence about new feature, change or fix information - Extend and execute testsuite `tests/run_all_tests.py` and debug if necessary - Update the `AUTHORS` file if necessary - Prefer to use `git rebase` instead of `git merge`, to keep a cleaner commit history - Test your code before sending a PR ### JUBE Contributor License Agreement Thank you for your interest in contributing to JUBE ("We" or "Us"). The purpose of this contributor agreement is to clarify and document the rights granted by contributors to Us. To make this document effective, please follow the instructions below. #### How to use this CLA If You do not own the Copyright in the entire work of authorship, any other author of the Contribution must also sign this. If you contribute outside of any legal obligations towards third parties, you may do so without the additional steps below. If You are an employee and have created the Contribution as part of your employment, You need to have Your employers approval as a Legal Entity. If you are a Legal Entity you must provide and update a list of your employees that will contribute to the Material, as well as your contact information. All contributions of employees must be individualy attributable to each one. #### Definitions "You" means the individual Copyright owner who Submits a Contribution to Us. "Legal Entity" means an entity that is not a natural person. "Contribution" means any original work of authorship, including any original modifications or additions to an existing work of authorship, Submitted by You to Us, in which You own the Copyright. "Copyright" means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence. "Material" means the software or documentation made available by Us to third parties. When this Agreement covers more than one software project, the Material means the software or documentation to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. "Submit" means any act by which a Contribution is transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." "Documentation" means any non-software portion of a Contribution. #### Rights and Obligations You hereby grant to Us a worldwide, royalty-free, non-exclusive, perpetual and irrevocable license, with the right to transfer an unlimited number of licenses or to grant sublicenses to third parties, under the Copyright covering the Contribution to use the Contribution by all means. We agree to (sub)license the Contribution or any Materials containing it, based on or derived from your Contribution non-exclusively under the terms of any licenses the Free Software Foundation classifies as Free Software License, and which are approved by the Open Source Initiative as Open Source licenses. #### Copyright You warrant, that you are legitimated to license the Contribution to us. #### Acceptance of the CLA By submitting your Contribution via the Repository you accept this agreement. #### Liability Except in cases of willful misconduct or physical damage directly caused to natural persons, the Parties will not be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Material, including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Material. #### Warranty The Material is a work in progress, which is continuously being improved by numerous Contributors. It is not a finished work and may therefore contain defects or "bugs" inherent to this type of development. For the above reason, the Material is provided on an "as is" basis and without warranties of any kind concerning the Material, including without limitation, merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright. #### Miscellaneous This Agreement and all disputes, claims, actions, suits or other proceedings arising out of this agreement or relating in any way to it shall be governed by the laws of Germany excluding its private international law provisions. If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and that is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. You agree to notify Us of any facts or circumstances of which You become aware that would make this Agreement inaccurate in any respect.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/JUBE.egg-info/0000775000174700017470000000000014603772011016650 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/JUBE.egg-info/PKG-INFO0000664000174700017470000000363414603772010017752 0ustar00gitlab-runnergitlab-runnerMetadata-Version: 2.1 Name: JUBE Version: 2.6.2 Summary: JUBE Benchmarking Environment Home-page: www.fz-juelich.de/ias/jsc/jube Author: Forschungszentrum Juelich GmbH Author-email: jube.jsc@fz-juelich.de License: GPLv3 Download-URL: www.fz-juelich.de/ias/jsc/jube Keywords: JUBE Benchmarking Environment Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3.2 Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Benchmark Classifier: Topic :: Software Development :: Testing License-File: LICENSE License-File: AUTHORS Automating benchmarks is important for reproducibility and hence comparability which is the major intent when performing benchmarks. Furthermore managing different combinations of parameters is error-prone and often results in significant amounts work especially if the parameter space gets large. In order to alleviate these problems JUBE helps performing and analyzing benchmarks in a systematic way. It allows custom work flows to be able to adapt to new architectures. For each benchmark application the benchmark data is written out in a certain format that enables JUBE to deduct the desired information. This data can be parsed by automatic pre- and post-processing scripts that draw information, and store it more densely for manual interpretation. The JUBE benchmarking environment provides a script based framework to easily create benchmark sets, run those sets on different computer systems and evaluate the results. It is actively developed by the Juelich Supercomputing Centre of Forschungszentrum Juelich, Germany. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/JUBE.egg-info/SOURCES.txt0000664000174700017470000000653314603772010020542 0ustar00gitlab-runnergitlab-runnerAUTHORS CITATION.cff CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE README.md RELEASE_NOTES setup.py JUBE.egg-info/PKG-INFO JUBE.egg-info/SOURCES.txt JUBE.egg-info/dependency_links.txt JUBE.egg-info/requires.txt JUBE.egg-info/top_level.txt bin/jube bin/jube-autorun contrib/schema/jube.dtd contrib/schema/jube.json contrib/schema/jube.rnc contrib/schema/jube.xsd docs/JUBE.pdf examples/cycle/cycle.xml examples/cycle/cycle.yaml examples/dependencies/dependencies.xml examples/dependencies/dependencies.yaml examples/do_log/do_log.xml examples/do_log/do_log.yaml examples/do_log/loreipsum1 examples/do_log/loreipsum2 examples/do_log/loreipsum3 examples/do_log/loreipsum4 examples/do_log/loreipsum5 examples/duplicate/duplicate.xml examples/duplicate/duplicate.yaml examples/environment/environment.xml examples/environment/environment.yaml examples/files_and_sub/file.in examples/files_and_sub/files_and_sub.xml examples/files_and_sub/files_and_sub.yaml examples/hello_world/hello_world.xml examples/hello_world/hello_world.yaml examples/include/include_data.xml examples/include/include_data.yaml examples/include/main.xml examples/include/main.yaml examples/iterations/iterations.xml examples/iterations/iterations.yaml examples/jobsystem/job.run.in examples/jobsystem/jobsystem.xml examples/jobsystem/jobsystem.yaml examples/parallel_workpackages/parallel_workpackages.xml examples/parallel_workpackages/parallel_workpackages.yaml examples/parameter_dependencies/include_file.xml examples/parameter_dependencies/include_file.yaml examples/parameter_dependencies/parameter_dependencies.xml examples/parameter_dependencies/parameter_dependencies.yaml examples/parameter_update/parameter_update.xml examples/parameter_update/parameter_update.yaml examples/parameterspace/parameterspace.xml examples/parameterspace/parameterspace.yaml examples/result_creation/result_creation.xml examples/result_creation/result_creation.yaml examples/result_database/result_database.xml examples/result_database/result_database.yaml examples/result_database/result_database_filter.xml examples/scripting_parameter/scripting_parameter.xml examples/scripting_parameter/scripting_parameter.yaml examples/scripting_pattern/scripting_pattern.xml examples/scripting_pattern/scripting_pattern.yaml examples/shared/shared.xml examples/shared/shared.yaml examples/statistic/statistic.xml examples/statistic/statistic.yaml examples/tagging/tagging.xml examples/tagging/tagging.yaml examples/yaml/hello_world.yaml examples/yaml/special_values.yaml jube2/__init__.py jube2/analyser.py jube2/benchmark.py jube2/completion.py jube2/conf.py jube2/fileset.py jube2/help.py jube2/help.txt jube2/info.py jube2/jubeio.py jube2/log.py jube2/main.py jube2/parameter.py jube2/pattern.py jube2/result.py jube2/step.py jube2/substitute.py jube2/workpackage.py jube2/result_types/__init__.py jube2/result_types/database.py jube2/result_types/genericresult.py jube2/result_types/keyvaluesresult.py jube2/result_types/syslog.py jube2/result_types/table.py jube2/util/__init__.py jube2/util/output.py jube2/util/util.py jube2/util/version.py jube2/util/yaml_converter.py platform/lsf/platform.xml platform/lsf/submit.job.in platform/moab/chainJobs.sh platform/moab/platform.xml platform/moab/submit.job.in platform/pbs/chainJobs.sh platform/pbs/platform.xml platform/pbs/submit.job.in platform/slurm/chainJobs.sh platform/slurm/platform.xml platform/slurm/submit.job.in././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/JUBE.egg-info/dependency_links.txt0000664000174700017470000000000114603772010022715 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/JUBE.egg-info/requires.txt0000664000174700017470000000000714603772010021244 0ustar00gitlab-runnergitlab-runnerpyyaml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/JUBE.egg-info/top_level.txt0000664000174700017470000000000614603772010021375 0ustar00gitlab-runnergitlab-runnerjube2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/LICENSE0000664000174700017470000010451314603772010015441 0ustar00gitlab-runnergitlab-runner GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/PKG-INFO0000664000174700017470000000363414603772011015534 0ustar00gitlab-runnergitlab-runnerMetadata-Version: 2.1 Name: JUBE Version: 2.6.2 Summary: JUBE Benchmarking Environment Home-page: www.fz-juelich.de/ias/jsc/jube Author: Forschungszentrum Juelich GmbH Author-email: jube.jsc@fz-juelich.de License: GPLv3 Download-URL: www.fz-juelich.de/ias/jsc/jube Keywords: JUBE Benchmarking Environment Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3.2 Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Benchmark Classifier: Topic :: Software Development :: Testing License-File: LICENSE License-File: AUTHORS Automating benchmarks is important for reproducibility and hence comparability which is the major intent when performing benchmarks. Furthermore managing different combinations of parameters is error-prone and often results in significant amounts work especially if the parameter space gets large. In order to alleviate these problems JUBE helps performing and analyzing benchmarks in a systematic way. It allows custom work flows to be able to adapt to new architectures. For each benchmark application the benchmark data is written out in a certain format that enables JUBE to deduct the desired information. This data can be parsed by automatic pre- and post-processing scripts that draw information, and store it more densely for manual interpretation. The JUBE benchmarking environment provides a script based framework to easily create benchmark sets, run those sets on different computer systems and evaluate the results. It is actively developed by the Juelich Supercomputing Centre of Forschungszentrum Juelich, Germany. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/README.md0000664000174700017470000001030414603772010015705 0ustar00gitlab-runnergitlab-runner
JUBE
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7534372.svg)](https://doi.org/10.5281/zenodo.7534372) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) # What is JUBE? The JUBE benchmarking environment provides a script-based framework for easily creating benchmark and workflow sets, running those sets on different computer systems, and evaluating the results. It is actively developed by the [Juelich Supercomputing Centre](https://www.fz-juelich.de/en/ias/jsc). It focuses on managing the complexity of combinatorial benchmarks and ensuring reproducibility of the benchmarks. JUBE provides support for different workflows and the ability to use vendor-supplied platform configurations. The benchmark configuration and scripts can be specified in either YAML or XML format. JUBE is primarily designed for use on supercomputers with *scheduding* systems like Slurm or PBS, but also works on laptops running Linux or MacOS operating systems. ## Documentation JUBE is not (yet) available on `pypi` (it is work in progress). The source code can be downloaded from any of the following places: - [GitHub](https://github.com/FZJ-JSC/JUBE) - [JSC JUBE Webpage](https://www.fz-juelich.de/en/ias/jsc/services/user-support/software-tools/jube/download) JUBE can be installed using `pip` or `setup.py` and needs *python 3.2* or higher. You will also need *SQLite* version 3.35.0 (or higher) to use the database as a result output. Installation instructions can be found [here](https://apps.fz-juelich.de/jsc/jube/jube2/docu/tutorial.html#installation). The documentation for JUBE is split into Beginner Tutorial, Advanced Tutorial, FAQ, CLI, and Glossary and can be found in the **[User Guide](https://apps.fz-juelich.de/jsc/jube/jube2/docu/index.html)**. In addition to the documentation, there are also [tutorial examples](examples) which are described in the tutorials of the user guide and [benchmark examples](https://github.com/FZJ-JSC/jube-configs), which are curated examples of JUBE benchmarks (the latter will be either replaced or updated/extended soon). For more information on the design and architecture of JUBE, please refer to this [paper](https://ebooks.iospress.nl/DOI/10.3233/978-1-61499-621-7-431). ## Community and Contributing JUBE is an open-source project and we welcome your questions, discussions and contributions. Questions can be asked directly to the JSC JUBE developers via mail to [jube.jsc@fz-juelich.de](mailto:jube.jsc@fz-juelich.de) and issues can be reported in the issue tracker. We also welcome contributions in the form of pull requests. Contributions can include anything from bug fixes and documentation to new features. JUBE development is currently still taking place on an internal GitLab instance. However, we are in a transition phase to move development to GitHub. The complete move will take some time. In the meantime, we will decide individually how to proceed with Pull Requests opened on GitHub. Before you start implementing new features, we would recommended to contact us, as we still have several open branches in GitLab. - **[GitHub Issue Tracker](https://github.com/FZJ-JSC/JUBE/issues)** - **[Github Discussions](https://github.com/FZJ-JSC/JUBE/discussions)** - **[GitHub Pull Requests](https://github.com/FZJ-JSC/JUBE/pulls)** Please ensure that your contributions to JUBE are compliant with the [contribution](CONTRIBUTING.md), [developer](https://apps.fz-juelich.de/jsc/jube/jube2/docu/devel.html) and [community](CODE_OF_CONDUCT.md) guidelines. # Citing JUBE If you use JUBE in your work, please cite the [software release](https://zenodo.org/records/7534372) and the [paper](https://ebooks.iospress.nl/DOI/10.3233/978-1-61499-621-7-431). # Acknowledgments We gratefully acknowledge the support of the following research projects and institutions in the development of JUBE and for granting compute time to develop JUBE. - UNSEEN (BMWi project, ID: 03EI1004A-F) - Gauss Centre for Supercomputing e.V. (www.gauss-centre.eu) and the John von Neumann Institute for Computing (NIC) on the GCS Supercomputer JUWELS at Jülich Supercomputing Centre (JSC) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/RELEASE_NOTES0000664000174700017470000006006314603772010016410 0ustar00gitlab-runnergitlab-runnerRelease notes ************* Version 2.6.2 ============= Release: 2024-04-05 * Added: *CONTRIBUTING.md* file * Added: New *parameter* named *jobname* in *parameterset* *systemParameter* of file *platform/slurm/platform.xml* to modify the value of the *--job-name* argument in *submit.job*. During substitution in *executesub*, quotes are added around the value of *jobname*. * Added: Test to verify that a *step* with a *do* operation that contains a *done_file* has been successfully executed. * Added: *CODE_OF_CONDUCT.md* file * Added: JUBE logo in svg and eps format * Fixed: Resolved deprecation warnings * Fixed: Parameters of a dependent step with an update_mode *step* are updated when a *jube continue* is executed. * Fixed: Allow YAML and XML scripts to include paths per *include* in *include-path*. * Fixed: Use correct *continue* log file for parallel *step* execution. * Deprecated: Comma character (*,*) will not be supported anymore in *tag* attributes. Instead use *|*, *+* and *!*. * Changed: The *unit* of a *parameter* or *pattern* will also be displayed in the results output when a *title* is specified. * Changed: Redesign of the *README.md* file * Changed: Colors on website Version 2.6.1 ============= Release: 2023-11-28 * Added: CITATION.cff file * Added: New argument *-w* for command line option *jube info* to print information about the given workpackage. * Fixed: Overwrite the *sub* with the same *source*, so that the substitution with *init_with* will work again. * Changed: The *substitute_tests* has been adapted so that the tests fail when the bug fixed in this release occurs. Version 2.6.0 ============= Release: 2023-11-16 * Added: New option **, which allows you to specify tags that must be set for the script to run. * Added: New optional *mode* attribute to the *sub*-tag, allowing *regex* substitution as an alternative to *text*-based substitution. The latter remains the default. * Added: The *jube result* command line call has been extended by the options *--select* and *--exclude* to show and hide selected result columns. * Added: New command line option *jube output* to print out the path and the contents of the files stdout and stderr. * Added: A new *unit* attribute to *parameter*, which has the same function as the *unit* attribute of *pattern*. * Added: New jube variable *$jube_wp_status* which contains the status of the current workpackage at the time the variable is evaluated. * Added: New YAML schema file. * Added: Missing *database*-tag to dtd, rnc and xsd schema files. (Issue #1 on GitHub) * Added: A validity check by use of the pip package ruamel.yaml is introduced. This package is used if it is installed. Otherwise, there will be no validity check for yaml files. * Fixed: The *include-path* for YAML scripts and added an usage example to the advanced tutorial. (Issue #2 on GitHub) * Fixed: *export* of an empty *parameter*. (Issue #3 on GitHub) * Fixed: Backward compatibility for floating number value definitions of parameters of type int was restored such that only warning messages are printed in this case. * Fixed: Typos and bugs in the glossary and the help message. * Changed: The *files_and_sub*, *parallel_workpackages*, *result_creation*, *result_database* and *tagging* example has been updated. * Changed: The *jube complete* help output was debugged. * Changed: The tests for the examples have been completely rewritten to avoid using *run.log* files. * Changed: Improved CI: separated tests and automatic tasks for releases. Version 2.5.1 ============= Release: 2022-08-24 * The default behaviour of replacing two parameters with different options without throwing an error was restored. * The testing suite was extended. * The schema files were corrected such that they contain the duplicate option. Version 2.5.0 ============= Release: 2022-08-22 * Several independent workpackages within a step can be executed by multiple processes in parallel by stating *procs=#number_of_parallel_processes#* within the *step* tag. An example and a documentation entry was added. * A result database can be produced by use of the *database* tag. An example and a documentation entry was added. * *python2*-support was removed. * A couple of unittests were added which now include the testing of most of the examples. * Sample *run.log* of most examples were added to *tests/examples_output*. * Some yaml example scripts were corrected. * The MANIFEST file was removed. * A typo in the error message was fixed. * Fix result command documentation. * A wrong result entry in the glossary was fixed. * A bug for the usage of a newline separator within yaml scripts is resolved. * A feature to create a do_log file for every workpackage of a step is integrated. The do_log file contains the whole environment while execution, the execution shell, the change of current work directories, comments if a directive was executed in a shared fashion and the do directives of the steps. * The execution cancels now, when a parameter is of type int or float and the parameter value has not the form of a int or float correspondingly. * The FAQ documentation was extended with yaml examples. * The option duplicate for parametersets and parameters was introduced. Version 2.4.3 ============= Release: 2022-07-20 * Fixes a bug related to ** and *init-with* combinations. * *JUBE_EXEC_SHELL* is now also taken into account during parameter evaluation. * *jube status* now also returns *ERROR* state. * Fixes a bug of using *$$* in shell commands. * Updates *SLURM* *gres* default value in platform files. * Fixes a bug of having a list of benchmarks in YAML format. Version 2.4.2 ============= Release: 2021-11-30 * JUBE will raise an error if an changed *work_dir* contains unknown variables. * A bug was solved which enabled *dotall="true"* by default for all pattern, which can make those costly to evaluate. * Fixes a bug in result data processing. * Fixes a bug in YAML input format if *benchmark* key is not used. * A empty value in YAML input format will now be treated liek an empty String not as a *None* value. * Avoid crash due to overflow error for huge pattern values. * Fixes a bug, which blocked *include* blocks to include other *include* blocks. * *setup.py* now moves all additional non-code data to *.../share/jube*, which allows better utilization of *pip* based installation Version 2.4.1 ============= Release: 2021-02-09 * A bug was solved, if a benchmark used the older *,*-separated *tag=* format in contrast to the new layout introduced in *version 2.2.2*. * A warning message in context of newer *YAML* versions was removed. * A Python3 problem inside the *YAML* parser was solved. * A bug was solved, which was raised if the benchmark was started on a different filesystem then the one which was configured within *outpath*. * The *jube* base script within *bin* will now use *python3* by default. This is necessary as many newer systems does not have a "standard" *python* defined by default. In addition the additional script *jube-python2* is now available, which utilizes *python2*. So far Python 2 is still fully supported but can be seen deprecated and future versions of *JUBE* might break the Python 2 backwards compatibility. * All *style=pretty* tables in *JUBE* will now use a markdown like format to allow easier integration within other tools. Version 2.4.0 ============= Release: 2020-07-03 * New *YAML* based *JUBE* input format. The existing *XML* format still stays available. Both formats cover the same amount of features. If you plan to use *YAML* based *JUBE* input files, you have to add the pyyaml-module to your *Python* module library. See also Input format * New "" attribute: "error_file="..."". In contrast to the existing "done_file" this file handle can be used to mark a broken asynchronous execution (the job templates in the "platform" folder were updated accordingly) * The "analyse" step is now automatically called when a result is shown and if it was not executed before (instead of showing an error message). * New option "--workpackage" for "remove" command line sub command. Allows to remove an individual workpackage from a benchmark. See also: Restart a workpackage execution * New "table" output format: "aligned" Version 2.3.0 ============= Release: 2019-11-07 * New command line option "-s {pretty,csv}, --style {pretty,csv}" for the "result" command allows to overwrite the selected table style * New command line option "-o OUTPATH, --outpath OUTPATH" for the "run" command allows to overwrite the selected outpath for the benchmark run * New parameter modes: "env" and "tag" * "mode="env": include the content of an available environment variable * "mode="tag": include the tag name if the tag was set during execution, otherwise the content is empty * New option "dotall=true" in "" (default: "false") allows that "." within a regular expression also matches newline characters. This can be very helpfull to extract a line only after a specific header was mentioned. See Extract data from a specifc text block * "--tags" used in combination with the "--update" option will now be added to the existing tags of the original run instead of overwriting the old tags. If no new tags need to be added within an update "--tags" can now be skipped. * "parse.log" is now automatically moved into the specifc job run folder and is also available within the "jube log" command Version 2.2.2 ============= Release: 2019-02-04 * New "tag" handling: Tags can now be mixed by using boolean operations ("+" for and, "|" for or), brackets are allowed as well. Old "," separated lists of tags are automatically converted. See Tagging * Extend parameter update documentation. See Parameter update * Platform files were renamed (system specific to queuing system specific) * Fix "$jube_wp_relpath" and "$jube_wp_abspath" if *JUBE* is executed from a relative directory * Fixed missing or wrong environment variable evaluation within *JUBE* parameters * Fix for derived pattern handling if no match for regular pattern was found * Fix default value handling for derived pattern * Fix unicode decoding problems for environment variables Version 2.2.1 ============= Release: 2018-06-22 * Allow separator selection when using the "jube info ... -c" option * Fix internal handling if a script parameter or a template is evaluated to an empty value * Fix for different Python3 parsing conflicts Version 2.2.0 ============= Release: 2017-12-21 * New feature: step cycles. See Step cycle * New parameter "update_mode". See Parameter update * Result creation by scanning multiple steps now automatically creates a combined output * Speed up of the *JUBE* internal management if a large number of work packages is used * *JUBE* 1 conversion tool is not available any more * New general commandline option "--strict" stops *JUBE* if there is a version mismatch * Broken analysis files will now be ignored * Fix combination of "active" and "shared" * Fix sorting problem for multiple result columns * Fix parameter problem, if the continue command is used and the parameter holds a value having multiple lines Version 2.1.4 ============= Release: 2016-12-20 * "--id" indices on the commandline can now be negative to count from the end of the available benchmarks * *JUBE* now allows a basic auto completion mechanism if using *BASH*. To activate: "eval "$(jube complete)"" * Fix result sorting bug in Python3 * New "jube_benchmark_rundir" variable which holds the top level *JUBE* directory (the absolute "outpath" directory) * Fix CSV output format, if parameter contain linebreaks. * "active" attribute can now be used in "", "" and "" * New FAQ entry concerning multiple file analysis: Frequently Asked Questions * "" using "mode="shell"" or "mode="perl"" will now stop program execution if an error occurs (similar to "mode="python"") * "" specfic "work_dir" is now created automatically if needed * "directory" attribute in "" and "" was renamed to "source_dir" (old attribute name is still possible) * "source_dir" now allows parameter substitution * New attribute "target_dir" in "" and "" to specify the target directory path prefix Version 2.1.3 ============= Release: 2016-09-01 * Fix broken CSV table output style * Fix "jube_wp_..." parameter handling bug, if these parameter are used inside another script parameter * Added new optional argument "suffix="..."" to the "" tag * Parameter are allowed inside this argument string. * The evaluated string will be attached to the default workpackage directory name to allow users to find specific directories in an easier way (e.g. "000001_stepname_suffix" ). * The *XML* schema files can now be found inside the "contrib" folder * Added new advanced error handling * JUBE will not stop any more if an error occurs inside a "run" or "continue". The error will be marked and the corresponding workpackage will not be touched anymore. * There is also a "-e"/"--exit" option to overwrite this behaviour to directly exit if there is an error. Version 2.1.2 ============= Release: 2016-07-29 * The internal parameter handling is much faster now, especially if a large number of parameter is used within the same step. * Fix critical bug when storing environment variables. Environment variables wasn't read correctly inside a step if this step was only executed after a "jube continue" run. * Fix bug inside a "" if it contains any linebreak * Quotes are added automatically inside the "$jube_wp_envstr" variable to support spaces in the environment variable argument list * Combining "-u" and "tags" in a "jube result" run will not filter the result branches anymore * Allow lowercase "false" in bool expressions (e.g. the "active" option) * Fix bug when using *JUBE* in a *Python3.x* environment * The "jube help" output was restructed to display separate key columns instead of a keyword list * "" can now contain a "default=..." attribute which set their default value if the pattern can't be found or if it can't be evaluated * "null_value=..." was removed from the "" and ""-tag because the new default attribute matches its behaviour * Added first *JUBE* FAQ entries to the documentation: Frequently Asked Questions * New "active"-attribute inside a ""-tag. The attribute enables or disables the corresponding step (and all following steps). It can contain any bool expression and available parameter. * Fix bug in "" handling if an alternative link name is used which points to a sub directory * Added new option "-c / --csv-parametrization" to "jube info" command to show a workpackage specfic parametrisation by using the CSV format (similar to the existing "-p" option) * Allow Shell expansion in "" tags. "" now also support the "*" * Restructure internal "" and "" handling * All example platform files were updated an simplified Version 2.1.1 ============= Release: 2016-04-14 * *JUBE* will now show only the latest benchmark result by default, " --id all" must be used to see all results * Bool expressions can now be used directly in the "" attribute * Added "filter" attribute in "" and "" to show only specifix result entries (based on a bool expression) * New "" and "" mode: "mode="shell"" * Allow multiline output in result tables * Fix wrong group handling if "JUBE_GROUP_NAME" is used * Scripting parameter (e.g. "mode="python"") can now handle $ to allow access to environment variables * Fix $$ bug ($$ were ignored when used within a parameter) * Fix "$jube_wp_parent_..._id" bug if "$jube_wp_parent_..._id" is used within another parameter * Fix bug in std calculation when creating statistical result values * Fix bug if tags are used within "" Version 2.1.0 ============= Release: 2015-11-10 * Fix slow verbose mode * Fix empty debug output file * Fix broken command line "--include-path" option * Allow recursive "" and "" handling (additional include-paths can now be included by using the "" tag) * Allow multiple "" and "" areas * New "transpose="true"" attribute possible in "
" * Allow recursive parameter name creation in "" or "" (e.g. "${param${num}}") * Extend iteration feature * "iteration=#number" can be used in the "" tag, the work package will be executed #number times * New "reduce" attribute in analyser, possible values: "true" or "false" (default: "true") * "true": use a single result line to combine all iterations * "false": each iteration will get its separate result line * Fix pattern_cnt bug * New pattern suffix: "_std" (standard deviation) * "reduce" option in "" not needed anymore (all possible reduce options are now calculated automatically) * Fix jube-autorun and add progress check interval * Added "--force" command line option to skip *JUBE* version check * Added optional "out_mode" attribute in "". It can be "a" or "w" to allow appending or overwriting an existing "out"-file (default: "w"). * New version numbering model to divide between feature and bugfix releases Version 2.0.7 ============= Release: 2015-09-17 * *JUBE* will ignore folders in the benchmark directory which does not contain a "configuration.xml" * New pattern reduce example Statistic pattern values * New internal directory handling to allow more flexible feature addition * New internal result structure * Fix derived pattern bug when scanning multiple result files * *JUBE* version number will now be stored inside the "configuration.xml" * *JUBE* version number will be checked when loading an existing benchmark run * New *JUBE* variable: "$jube_wp_relpath" (contains relative workpackage path) * Add Verbose-Mode "-v" / "--verbose" * Enable verbose console output "jube -v run ..." * Show stdout during execution: "-vv" * Show log and stdout during execution: "-vvv" * Change version mode to "-V" / "--version" * "jube_parse.log" will now be created next to the ".xml" file * New syslog result type (thanks to Andy Georges for contribution), see *syslog_tag* * New environment variable "JUBE_GROUP_NAME": By setting and exporting "JUBE_GROUP_NAME" to an available UNIX group, *JUBE* will create benchmark directory structures which can be accessed by the given group. * Benchmark results can now be created also by user without write- access to the benchmark directory * Parametersets are now available within each dependent step. There is no need to reuse them anymore. Version 2.0.6 ============= Release: 2015-06-16 * users can now change the *JUBE* standard Shell ("/bin/sh") by using the new environment variable "JUBE_EXEC_SHELL", see Configuration * fixes a bug if a Shell filename completion results to a single file name (inside the ""-tag) * fixes stderr reading bug if "work_dir" was changed in a specific "" * changes include path order, new order: commandline ("--include-path ..."), config file (""), Shell var ("JUBE_INCLUDE_PATH"), "." * fixes some unicode issues * units in the result dataset will now be shown correctly if a file specific patternset is used Version 2.0.5 ============= Release: 2015-04-09 * "argparse" is now marked as a dependency in "setup.py". It will be automatically loaded when using *setuptools*. * tags will now also be used when including external sets by using "" * change default platform output filenames: using *job.out* and *job.err* instead of *stdout* and *stderr* for default job output * new internal workflow generation alogrithm * parameter can now be used in step "", e.g. "set_$number" * external sets had to be given by name to allow later substitution: "set$nr" * also multiple files can be mixed: "set$nr" * new example Parameter dependencies * allow "use"-attribute in file-tag to select file specific patternsets "" * Shell and parameter substitution now allowed in analyse files selection "*.log" * default "stdout" and "stderr" file will now stay in the default directory when changing the work_dir inside a "" * start of public available *JUBE* configuration files repository: https://github.com/FZJ-JSC/jube-configs Version 2.0.4 ============= Release: 2015-02-23 * fix bug when using *JUBE* in a *Python3.x* environment * time information (start, last modified) will now be stored in a seperate file and are not extracted out of file and directory metadata * "jube run" now allows the "--id/-i" command line option to set a specific benchmark id * "jube result" now automatically combines multiple benchmark runs within the same benchmark directory. *JUBE* automatically add the benchmark id to the result output (except only a specific benchmark was requested) * new command line option: "--num/-n" allow to set a maximum number of visible benchmarks in result * new command line option: "--revert/-r" revert benchmark id order * new attribute for "": "null_value="..."" to set a NULL representation for the output table (default: """") * new command: "jube update" checks weather the newest *JUBE* version is installed * new "id" options: "--id last" to get the last benchmark and "--id all" to get all benchmarks Version 2.0.3 ============= Release: 2015-01-29 * missing files given in a fileset will now raise an error message * "jube info --id --step " now also shows the current parametrization * "jube info --id --step -p" only shows the current parametrization using a csv table format * add new (optional) attribute "max_async="..."" to "": Maximum number of parallel workpackages of the correspondig step will run at the same time (default: 0, means no limitation) * switch "" to "" (also "" will be available) to avoid mixing of "s" and "z" versions * fix bug when using "," inside of a "" * *JUBE* now return a none zero error code if it sends an error message * update platform files to allow easier environment handling: "" will be automatically used inside of the corresponding jobscript * update platform jobscript templates to keep error code of running program * fix bug when adding ";" at the end of a "" * last five lines of stderr message will now be copied to user error message (if shell return code <> 0) * fix *Python2.6* compatibility bug in converter module * fix bug when using an evaluable parameter inside of another parameter Version 2.0.2 ============= Release: 2014-12-09 * fix a bug when using "init-with" to initialize a ""-tag * use "cp -p" behaviour to copy files * fix error message when using an empty "" * added error return code, if there was an error message Version 2.0.1 ============= Release: 2014-11-25 * "--debug" option should work now * fixes problem when including an external "" * update *Python 2.6* compatibility * all "" within a single "" now shares the same environment (including all exported variables) * a "" can export its environment to a dependent "" by using the new "export="true"" attribute (see new environment handling example) * update analyse behaviour when scanning multiple files (new "analyse" run needed for existing benchmarks) * in and out substitution files (given by "") can now be the same * "" now also supports multiline expressions inside the tag instead of the "dest"-attribute: "" Version 2.0.0 ============= Release: 2014-11-14 * complete new **Python** kernel * new input file format * please see new documentation to get further information ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/bin/0000775000174700017470000000000014603772011015201 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/bin/jube0000775000174700017470000000200414603772010016047 0ustar00gitlab-runnergitlab-runner#!/usr/bin/env python3 # JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Executable script for main program""" from __future__ import (print_function, unicode_literals, division) import jube2.main if __name__ == "__main__": jube2.main.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/bin/jube-autorun0000775000174700017470000000624014603772010017550 0ustar00gitlab-runnergitlab-runner#!/usr/bin/env bash # JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . OUTPUT_FILE=jube_job_output.txt ONLY_RESULT_OUTPUT=0 PROGRESS_INTERVAL=30 # Remove existing output file if [ -f $OUTPUT_FILE ] then rm $OUTPUT_FILE fi function print_usage () { echo "usage: ${0##*/} [OPTIONS] BENCHMARK_CONFIG_FILE" echo "This script automates full benchmark execution, including" echo "steps the run asynchronously, e.g. in a batch system." echo "" echo "Options:" echo " -r ARG additional run args" echo " -c ARG additional continue args" echo " -a ARG additional analyse args" echo " -s ARG additional result args" echo " -p ARG progress check interval in seconds (default:30)" echo " -o only show result output" echo "Example: ${0##*/} input_file.xml" } # Parse optional arguments while getopts r:c:a:s:p:o OPT; do case $OPT in r) RUN_ARG="$OPTARG";; c) CONTINUE_ARG="$OPTARG";; a) ANALYSE_ARG="$OPTARG";; s) RESULT_ARG="$OPTARG";; p) PROGRESS_INTERVAL="$OPTARG";; o) ONLY_RESULT_OUTPUT=1;; *) print_usage exit 2 esac done shift $(( OPTIND - 1 )) OPTIND=1 # check if input file exists if [ $# -lt 1 ] then echo "$0: missing argument" print_usage exit 1 fi # start benchmark execution if [ $ONLY_RESULT_OUTPUT -eq 1 ] then jube --force run $1 --hide-animation $RUN_ARG 2>&1 >> $OUTPUT_FILE else jube --force run $1 --hide-animation $RUN_ARG 2>&1 | tee -a $OUTPUT_FILE fi # extract benchmark dir BENCHMARK_DIR=`egrep -o 'handle: .+$' jube_job_output.txt | cut -c9-` # BENCHMARK_DIR must exist if [ ! -d "$BENCHMARK_DIR" ] then exit 1 fi # continue benchmark execution while [ `jube status $BENCHMARK_DIR` = "RUNNING" ] do sleep $PROGRESS_INTERVAL if [ $ONLY_RESULT_OUTPUT -eq 1 ] then jube --force continue $BENCHMARK_DIR --hide-animation $CONTINUE_ARG 2>&1 >> $OUTPUT_FILE else echo "Update benchmark information (`date`)" jube --force continue $BENCHMARK_DIR --hide-animation --id last $CONTINUE_ARG | tee -a $OUTPUT_FILE fi done # benchmark analyse if [ $ONLY_RESULT_OUTPUT -eq 1 ] then jube --force analyse $BENCHMARK_DIR --id last $ANALYSE_ARG 2>&1 >> $OUTPUT_FILE else jube --force analyse $BENCHMARK_DIR --id last $ANALYSE_ARG | tee -a $OUTPUT_FILE fi # create benchmark result jube --force result $BENCHMARK_DIR --id last $RESULT_ARG 2>&1 | tee -a $OUTPUT_FILE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5629401 JUBE-2.6.2/contrib/0000775000174700017470000000000014603772011016071 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/contrib/schema/0000775000174700017470000000000014603772011017331 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/contrib/schema/jube.dtd0000664000174700017470000001753114603772010020761 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/contrib/schema/jube.json0000664000174700017470000010511714603772010021155 0ustar00gitlab-runnergitlab-runner{ "$schema": "http://json-schema.org/draft-07/schema", "title": "JUBE", "anyOf": [{ "description": "JUBE script/benchmark definition. It is a container for all benchmark information.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/benchmark_object" } }, { "$ref": "#/$defs/benchmark_object" }] }, { "$ref": "#/$defs/jube_object" }], "$defs": { "jube_object": { "description": "JUBE script definition", "type": "object", "properties": { "include-path": { "description": "It is used to add some include paths where to search for include files.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/include-path_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/include-path_object" }, { "type": "string" }] }, "selection": { "description": "It is used to select or unselect *benchmarks* by name.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/selection_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/selection_object" }, { "type": "string" }] }, "parameterset": { "$ref": "#/$defs/parameterset" }, "patternset": { "$ref": "#/$defs/patternset" }, "substituteset": { "$ref": "#/$defs/substituteset" }, "fileset": { "$ref": "#/$defs/fileset" }, "benchmark": { "description": "The main benchmark definition. It is a container for all benchmark information.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/benchmark_object" } }, { "$ref": "#/$defs/benchmark_object" }] }, "include": { "$ref": "#/$defs/include" }, "version": { "description": "Version of the JUBE script", "type": "string" } }, "additionalProperties": false }, "include-path_object": { "type": "object", "properties": { "path": { "description": "The path will be scanned for include files.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" }, "tag": { "$ref": "#/$defs/tag" } }, "additionalProperties": false }, "selection_object": { "type": "object", "properties": { "only": { "description": "Only selected *benchmarks* will run. It can contain a name list divided by ','.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] }, "not": { "description": "Selected *benchmarks* will not run. It can contain a name list divided by ','.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] }, "tag": { "$ref": "#/$defs/tag" } }, "additionalProperties": false }, "benchmark_object": { "type": "object", "properties": { "name": { "description": "Unique name of the benchmark", "type": "string" }, "outpath": { "description": "It contains the path to the root folder for benchmark runs. The path will be relative to the input file location. Inside this given outpath every benchmark and every (new) run will create a new folder.", "type": "string" }, "comment": { "description": "It is used to add a benchmark specific comment. These comment will be stored inside the benchmark directory.", "type": "string" }, "check_tags": { "description": "It specifies a combination of tags that must be set (logical operations: \"+\" (conjunction), \"|\" (disjunction), \"!\" (negation))", "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "string" }] }, "file_path_ref": { "description": "Unknown functionality", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "parameterset": { "$ref": "#/$defs/parameterset" }, "patternset": { "$ref": "#/$defs/patternset" }, "substituteset": { "$ref": "#/$defs/substituteset" }, "fileset": { "$ref": "#/$defs/fileset" }, "step": { "description": "A *step* gives a list of Shell operations and a corresponding parameter environment.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/step_object" } }, { "$ref": "#/$defs/step_object" }] }, "analyser": { "description": "The *analyser* describes the *steps* and files which should be scanned using a set of *pattern*.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/analyser_object" } }, { "$ref": "#/$defs/analyser_object" }] }, "result": { "description": "The *result* is used to handle different visualisation types of your analysed data.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/result_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/result_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name", "outpath" ], "additionalProperties": false }, "parameterset": { "description": "A *parameterset* is a container to store a bundle of *parameters*.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/parameterset_object" } }, { "$ref": "#/$defs/parameterset_object" }] }, "parameterset_object": { "type": "object", "properties": { "name": { "description": "Unique name of the *parameterset*", "type": "string" }, "init_with": { "description": "If the given filepath can be found inside of the JUBE_INCLUDE_PATH and if it contains a *parameterset* using the given name, all *parameters* will be copied to the local set.", "type": "string" }, "duplicate": { "description": "It is of relevance, if there are more than one *parameter* definitions with the same name within one *parameterset*. This duplicate option has lower priority than the duplicte option of the *parameters*. It must contain one of the following three options: replace, concat, error", "type": "string", "pattern": "^(replace|concat|error)$" }, "tag": { "$ref": "#/$defs/tag" }, "parameter": { "description": "A *parameter* can be used to store *benchmark* configuration data. A set of different *parameters* will create a specific parameter environment (also called parameter space) for the different *steps* of the *benchmark*.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/parameter_object" } }, { "$ref": "#/$defs/parameter_object" }] }, "include": { "$ref": "#/$defs/include" } }, "required": "name", "additionalProperties": false }, "parameter_object": { "type": "object", "properties": { "name": { "description": "The name must be unique inside the given *parameterset*.", "type": "string" }, "type": { "description": "It is only used for sorting. Following types are allowed: string, int, float", "type": "string", "pattern": "^(string|int|float)$" }, "mode": { "description": "The mode is used for script-types. Following modes are allowed: python, perl, shell, env, tag, text", "type": "string", "pattern": "^(python|perl|shell|env|tag|text)$" }, "export": { "description": "If export is set to true, the *parameter* will be exported to the shell environment when using *do*.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "unit": { "description": "The unit will be used in the result table.", "type": "string" }, "duplicate": { "description": "It is of relevance, if there are more than one *parameter* definitions with the same name within one *parameterset*. This duplicate option has higher priority than the duplicte option of the *parameterset*. It must contain one of the following four options: none, replace , concat , error", "type": "string", "pattern": "^(none|replace|concat|error)$" }, "update_mode": { "description": "Depending on the update_mode the *parameter* will be reevaluated. Following settings are allowed: never, use, step, cycle, always", "type": "string", "pattern": "^(never|use|step|cycle|always)$" }, "separator": { "description": "The default separator ',' can be changed by using this.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "Real content of the *parameter*", "type": ["string", "number"] } }, "required": "name", "additionalProperties": false }, "substituteset": { "description": "A *substituteset* is a container to store a bundle of *sub* commands.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/substituteset_object" } }, { "$ref": "#/$defs/substituteset_object" }] }, "substituteset_object": { "type": "object", "properties": { "name": { "description": "Unique name of the *substituteset*", "type": "string" }, "init_with": { "description": "If the given filepath can be found inside of the JUBE_INCLUDE_PATH and if it contains a *substituteset* using the given name, all *iofile* and *sub* will be copied to the local set.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "iofile": { "description": "A *iofile* declares the name (and path) of a file used for substitution.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/iofile_object" } }, { "$ref": "#/$defs/iofile_object" }] }, "sub": { "description": "A substitution expression", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/sub_object" } }, { "$ref": "#/$defs/sub_object" }] }, "include": { "$ref": "#/$defs/include" } }, "required": "name", "additionalProperties": false }, "iofile_object": { "type": "object", "properties": { "in": { "description": "Relative filepath to the current work directory for every single *step*. It can be the same as *out*.", "type": "string" }, "out": { "description": "Relative filepath to the current work directory for every single *step*. It can be the same as *in*.", "type": "string" }, "out_mode": { "description": "It can be used to declare, if the out-file will be overridden (w) or appended (a).", "type": "string", "pattern": "^(w|a)$" }, "tag": { "$ref": "#/$defs/tag" } }, "required": [ "in","out" ], "additionalProperties": false }, "sub_object": { "type": "object", "properties": { "source": { "description": "The source-string will be replaced by *dest*.", "type": "string" }, "dest": { "description": "The dest-string will replace the *source*.", "type": "string" }, "_": { "description": "It will replace the *source* (should be used for multiline output).", "type": "string" }, "mode": { "description": "It can be used to switch between \"text\" and \"regex\" substitution (default: \"text\").", "type": "string", "pattern": "^(text|regex)$" }, "tag": { "$ref": "#/$defs/tag" } }, "required": [ "source" ], "additionalProperties": false, "not": { "required": [ "dest", "_"] } }, "fileset": { "description": "A *fileset* is a container to store a bundle of *links* and *copy* commands.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/fileset_object" } }, { "$ref": "#/$defs/fileset_object" }] }, "fileset_object": { "type": "object", "properties": { "name": { "description": "The name must be unique.", "type": "string" }, "init_with": { "description": "If the given filepath can be found inside of the JUBE_INCLUDE_PATH and if it contains a *fileset* using the given name, all *link* and *copy* will be copied to the local set.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "copy": { "description": "A *copy* can be used to copy a file or directory from your normal filesytem to your sandbox work directory. It can contain a list of filenames (or directories).", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/file_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/file_object" }, { "type": "string" }] }, "link": { "description": "A *link* can be used to create a symbolic link from your sandbox work directory to a file or directory inside your normal filesystem. It can contain a list of filenames (or directories).", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/file_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/file_object" }, { "type": "string" }] }, "prepare": { "description": "It can contain any Shell command you want. It will be executed like a normal *do* inside the *step* where the corresponding *fileset* is used. The only difference towards the normal *do* is, that it will be executed before the substitution will be executed.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/prepare_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/prepare_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "file_object": { "type": "object", "properties": { "directory": { "description": "It will be used as a prefix for the source filenames. It does not allow *parameter* substitution (deprecated; new attribut: *source_dir*).", "type": "string" }, "name": { "description": "It can be used to rename the file inside your work directory (it will be ignored if you use shell extensions in your pathname).", "type": "string" }, "rel_path_ref": { "description": "It declares, if relative paths will be based on the position of the JUBE script (external) or the current work directory (internal).", "type": "string", "pattern": "^(external|internal)$" }, "file_path_ref": { "description": "Unknown functionality", "type": "string" }, "active": { "description": "It can be used to enable or disable the single command.", "type": ["string", "boolean"] }, "source_dir": { "description": "It will be used as a prefix for the source filenames. It allows *parameter* substitution.", "type": "string" }, "target_dir": { "description": "It will be used as a prefix for the target filenames.", "type": "string" }, "separator": { "description": "It can be used to change the default separator ','.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "It can contain a list of filenames (or directories).", "type": "string" } }, "additionalProperties": false }, "prepare_object": { "type": "object", "properties": { "stdout": { "description": "Standard out file", "type": "string" }, "stderr": { "description": "Standard error file", "type": "string" }, "active": { "description": "It can be used to enable or disable the single command.", "type": ["string", "boolean"] }, "work_dir": { "description": "The work_dir can be used to change the work directory of this single command (relativly seen towards the original work directory).", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "It contains the Shell command that will be executed.", "type": "string" } }, "additionalProperties": false }, "patternset": { "description": "A *patternset* is a container to store a bundle of *patterns*.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/patternset_object" } }, { "$ref": "#/$defs/patternset_object" }] }, "patternset_object": { "type": "object", "properties": { "name": { "description": "Unique name of the *patternset*", "type": "string" }, "init_with": { "description": "If the given filepath can be found inside of the JUBE_INCLUDE_PATH and if it contains a *patternset* using the given name, all *pattern* will be copied to the local set.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "pattern": { "description": "A *pattern* is used to parse your output files and create your *result* data.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/pattern_object" } }, { "$ref": "#/$defs/pattern_object" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "pattern_object": { "type": "object", "properties": { "name": { "description": "The name must be unique.", "type": "string" }, "unit": { "description": "The unit will be used in the result table.", "type": "string" }, "type": { "description": "It is only used for sorting. Following types are allowed: string, int, float", "type": "string", "pattern": "^(string|int|float)$" }, "mode": { "description": "The mode is used for script-types. Following modes are allowed: python, perl, shell, env, tag, text", "type": "string", "pattern": "^(python|perl|shell|env|tag|text)$" }, "tag": { "$ref": "#/$defs/tag" }, "default": { "description": "It is used to specify a default value if *pattern* cannot be found or if it cannot be evaluated.", "type": ["string", "number"] }, "dotall": { "description": "It can be used to specify if a '.' within the regular expression should also match newline characters.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "_": { "description": "Real content of the *pattern*", "type": ["string", "number"] } }, "required": [ "name" ], "additionalProperties": false }, "step_object": { "type": "object", "properties": { "name": { "description": "The name must be unique.", "type": "string" }, "iterations": { "description": "Iterations can be used to execute all workpackages within this *step* multiple times.", "type": "integer" }, "max_async": { "description": "It can contain a number (or a *parameter*) which describe how many workpackages can be executed asynchronously (default: 0 means no limitation). This option is only important if a *do* inside the step contains a done_file attribute and should be executed in the background (or managed by a jobsystem).", "type": "string" }, "depend": { "description": "It can contain a list of other *step* names which must be executed before the current *step*.", "type": "string" }, "work_dir": { "description": "The work_dir can be used to switch to an alternative work directory.", "type": "string" }, "active": { "description": "It can be used to enable or disable the single command.", "type": ["string", "boolean"] }, "suffix": { "description": "The suffix can contain a string (*parameters* are allowed) which will be attached to the default workpackage directory name.", "type": "string" }, "export": { "description": "If export is set to true, the environment of the current *step* will be exported to an dependent *step*.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "shared": { "description": "This attribte can be used to create a shared folder which can be accessed by all workpackages based on this *step*.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "cycles": { "description": "Cycles can be used to execute all *do* commands within the *step* multiple times.", "type": "integer" }, "procs": { "description": "Amount of processes used to execute the *parameter* expansions of the corresponding *step* in parallel.", "type": "integer" }, "do_log_file": { "description": "Name or path of a do log file trying to mimick the *do* steps and the environment of a workpacakge of a *step* to produce an executable script.", "type": "string" }, "use": { "description": "It declares, which *parametersets*, *filesets* and *substitutionsets* are usable.", "$ref": "#/$defs/use" }, "do": { "description": "A *do* contains an executable Shell operation.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/do_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/do_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "use": { "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/use_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/use_object" }, { "type": "string" }] }, "use_object": { "type": "object", "properties": { "from": { "description": "The attribute can be used to specify an external set source.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "It contains the names of usable *sets* and/or *analysers*.", "type": "string" } }, "additionalProperties": false }, "do_object": { "type": "object", "properties": { "done_file": { "description": "By using done_file the user can mark async-steps. The operation will stop until the script will create the named file inside the work directory.", "type": "string" }, "error_file": { "description": "By using error_file the operation will produce a error if the named file can be found inside the work directory.", "type": "string" }, "break_file": { "description": "By using break_file the user can stop further cycle runs. the current *step* will be directly marked with finalized and further *do* will be ignored.", "type": "string" }, "stdout": { "description": "Standard out file", "type": "string" }, "stderr": { "description": "Standard error file", "type": "string" }, "active": { "description": "It can be used to enable or disable the single command.", "type": ["string", "boolean"] }, "shared": { "description": "If shared is set to true, the *do* will be executed inside the shared folder once (synchronize all workpackages).", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "work_dir": { "description": "The work_dir can be used to change the work directory of this single command (relativly seen towards the original work directory).", "type": "string" }, "_": { "description": "It contains the Shell command that will be executed.", "type": "string" } }, "additionalProperties": false }, "analyser_object": { "type": "object", "properties": { "name": { "description": "Unique name of the *analyser*", "type": "string" }, "reduce": { "description": "It declares, if the *result* lines for each iteration will be combined.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "tag": { "$ref": "#/$defs/tag" }, "use": { "description": "It declares, which *patternsets* are usable.", "$ref": "#/$defs/use" }, "analyse": { "description": "It declares, which *step* and files will be analysed.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/analyse_object" } }, { "$ref": "#/$defs/analyse_object" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "analyse_object": { "type": "object", "properties": { "step": { "description": "The *step* contains the *step* that will be analysed.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "file": { "description": "The file-attribute contains the files that will be analysed. Each file using each workpackage will be scanned seperatly.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/analyseFile_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/analyseFile_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "step" ], "additionalProperties": false }, "analyseFile_object": { "type": "object", "properties": { "use": { "description": "The use argument inside the file-tag can be used to specify a file specific *patternset*. The global *use* and this local *use* will be combined and evaluated at the same time.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "It contains the filenames of the files that will be analysed.", "type": "string" } }, "additionalProperties": false }, "result_object": { "description": "The *result* is used to handle different visualisation types of your analysed data.", "type": "object", "properties": { "result_dir": { "description": "The result_dir can be used to specify an different output directory. Inside of this directory a subfolder named by the current benchmark id will be created.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "use": { "description": "It declares, which *analyser* are usable.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] }, "table": { "description": "A simple ASCII based *table* ouput", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/table_object" } }, { "$ref": "#/$defs/table_object" }] }, "database": { "description": "Creates sqlite3 database", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/database_object" } }, { "$ref": "#/$defs/database_object" }] }, "syslog": { "description": "A syslog result type", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/syslog_object" } }, { "$ref": "#/$defs/syslog_object" }] }, "include": { "$ref": "#/$defs/include" } }, "additionalProperties": false }, "table_object": { "type": "object", "properties": { "name": { "description": "Unique name of the *table*", "type": "string" }, "style": { "description": "Allowed styles: csv, pretty, aligned", "type": "string", "pattern": "^(csv|pretty|aligned)$" }, "sort": { "description": "It can contain a list of parameter- or patternnames (separated by ,). Given pattern- or parametertypes will be used for sorting.", "type": "string" }, "filter": { "description": "It can contain a bool expression to show only specific result entries.", "type": "string" }, "separator": { "description": "This attribute can be used to change the default separator ',' (only used in csv-style).", "type": "string" }, "transpose": { "description": "If transpose is set to true, the *table* is transposed (rows and columns are swapped).", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "tag": { "$ref": "#/$defs/tag" }, "column": { "description": "A line within a ASCII result table. The *column* can contain the name of a *pattern* or the name of a parameter.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/column_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/column_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "database_object": { "type": "object", "properties": { "name": { "description": "The name of the table in the database.", "type": "string" }, "primekeys": { "description": "The primekeys can contain a list of *parameter* or *pattern* names (separated by ,). Given *parameters* or patterns will be used as primary keys of the database table. All primekeys have to be listed as a *key* as well. Modification of primary keys of an existing table is not supported. If no primekeys are set then each jube result will add new rows to the database. Otherwise rows with matching primekeys will be updated.", "type": "string" }, "file": { "description": "The given value should hold the full path to the database file. If the file including the path does not exists it will be created. Absolute and relative paths are supported.", "type": "string" }, "filter": { "description": "The filter can contain a bool expression to show only specific result entries.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "key": { "description": "The *key* must contain a single *parameter* or *pattern* name.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/string_tag_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false }, "syslog_object": { "type": "object", "properties": { "name": { "description": "The name for the *syslog*.", "type": "string" }, "address": { "description": "The socket adress that gives a syslog daemon (the combination of *host*/*port* and *address* is not allowed).", "type": "string" }, "host": { "description": "The host can give a syslog daemon in combination with *port* (the combination of *host*/*port* and *address* is not allowed).", "type": "string" }, "port": { "description": "The port can give a syslog daemon in combination with *host* (the combination of *host*/*port* and *address* is not allowed).", "type": "integer" }, "format": { "description": "The format can contain a log format written in a pythonic way.", "type": "string" }, "sort": { "description": "It can contain a list of parameter- or patternnames (separated by ,). Given pattern- or parametertypes will be used for sorting.", "type": "string" }, "filter": { "description": "The filter can contain a bool expression to show only specific result entries.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "key": { "description": "A syslog result key. It must contain a single parameter- or patternname.", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/column_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/column_object" }, { "type": "string" }] }, "include": { "$ref": "#/$defs/include" } }, "required": [ "name" ], "additionalProperties": false, "anyOf": [{ "allOf": [{ "not": { "required": [ "host" ] } }, { "not": { "required": [ "port" ] } }] }, { "allOf": [{ "not": { "required": [ "address" ] } }, { "required": [ "host","port" ] }] }] }, "column_object": { "type": "object", "properties": { "colw": { "description": "Column width", "type": "integer" }, "format": { "description": "The format can contain a C like format string: e.g. '.2f'", "type": "string" }, "title": { "description": "Alternative title", "type": "string" }, "tag": { "$ref": "#/$defs/tag" }, "_": { "description": "It can contain the name of a single pattern or parameter.", "type": "string" } }, "additionalProperties": false }, "include": { "description": "It can be used to include an external XML-structure into the current file.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/include_object" } }, { "$ref": "#/$defs/include_object" }] }, "include_object": { "type": "object", "properties": { "from": { "description": "The attribute is used to specify an external set source.", "type": "string" }, "path": { "description": "The path can be used to give an alternative xml-path inside the include-file.", "type": "string" }, "tag": { "$ref": "#/$defs/tag" } }, "required": [ "from" ], "additionalProperties": false }, "string_tag_object": { "type": "object", "properties": { "_": { "description": "Real content of the tag", "type": "string" }, "tag": { "$ref": "#/$defs/tag" } }, "additionalProperties": false }, "tag": { "description": "A *tag* can be used to mark parts of your input file to be includable or excludable ('not' *tags* are more important than normal *tags*). It can contain a list of names.", "type": "string" } } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/contrib/schema/jube.rnc0000664000174700017470000001706114603772010020766 0ustar00gitlab-runnergitlab-runner# added manually namespace xsi = "http://www.w3.org/2001/XMLSchema-instance" attlist.jube &= attribute xsi:noNamespaceSchemaLocation { text }? # rest was generated using 'trang jube.dtd jube.rnc' jube = element jube { attlist.jube, (selection | include-path | check_tags)*, (benchmark | parameterset | fileset | substituteset | patternset | \include)* } attlist.jube &= attribute version { text }? selection = element selection { attlist.selection, (only | not | tag)* } attlist.selection &= attribute tag { text }? only = element only { attlist.only, text } attlist.only &= attribute tag { text }? not = element not { attlist.not, text } attlist.not &= attribute tag { text }? tag = element tag { attlist.tag, text } attlist.tag &= attribute tag { text }? include-path = element include-path { attlist.include-path, mixed (path|include)* } attlist.include-path &= attribute tag { text }? path = element path { attlist.path, text } attlist.path &= attribute tag { text }? check_tags = element check_tags { attlist.check_tags, text } benchmark = element benchmark { attlist.benchmark, comment?, (parameterset | substituteset | fileset | step | patternset | analyzer | analyser | result | \include)* } attlist.benchmark &= attribute name { text }, attribute outpath { text }, attribute file_path_ref { text }?, attribute tag { text }? comment = element comment { attlist.comment, text } attlist.comment &= attribute tag { text }? parameterset = element parameterset { attlist.parameterset, (parameter | \include)* } attlist.parameterset &= attribute name { text }, attribute init_with { text }?, attribute duplicate { "replace" | "error" | "concat" }?, attribute tag { text }? parameter = element parameter { attlist.parameter, text } attlist.parameter &= attribute name { text }, attribute type { "int" | "string" | "float" }?, attribute mode { text }?, attribute export { "true" | "false" | "True" | "False" }?, attribute unit { text }?, attribute update_mode { "never" | "use" | "step" | "cycle" | "always" }?, attribute separator { text }?, attribute duplicate { "none" | "replace" | "error" | "concat" }?, attribute tag { text }? substituteset = element substituteset { attlist.substituteset, (iofile | sub | \include)* } attlist.substituteset &= attribute name { text }, attribute init_with { text }?, attribute tag { text }? iofile = element iofile { attlist.iofile, empty } attlist.iofile &= attribute in { text }, attribute out { text }, attribute out_mode { "w" | "a" }?, attribute tag { text }? sub = element sub { attlist.sub, any } attlist.sub &= attribute source { text }, attribute dest { text }?, attribute mode { "text" | "regex" }?, attribute tag { text }? fileset = element fileset { attlist.fileset, (copy | link | prepare | \include)* } attlist.fileset &= attribute name { text }, attribute init_with { text }?, attribute tag { text }? prepare = element prepare { attlist.prepare, text } attlist.prepare &= attribute stdout { text }?, attribute stderr { text }?, attribute active { text }?, attribute work_dir { text }?, attribute tag { text }? link = element link { attlist.link, text } attlist.link &= attribute directory { text }?, attribute source_dir { text }?, attribute target_dir { text }?, attribute name { text }?, attribute rel_path_ref { "internal" | "external" }?, attribute file_path_ref { text }?, attribute separator { text }?, attribute active { text }?, attribute tag { text }? copy = element copy { attlist.copy, text } attlist.copy &= attribute directory { text }?, attribute source_dir { text }?, attribute target_dir { text }?, attribute name { text }?, attribute rel_path_ref { "internal" | "external" }?, attribute file_path_ref { text }?, attribute separator { text }?, attribute active { text }?, attribute tag { text }? patternset = element patternset { attlist.patternset, (pattern | \include)* } attlist.patternset &= attribute name { text }, attribute init_with { text }?, attribute tag { text }? pattern = element pattern { attlist.pattern, text } attlist.pattern &= attribute name { text }, attribute type { "int" | "string" | "float" }?, attribute mode { text }?, attribute unit { text }?, attribute default { text }?, attribute dotall { "true" | "false" | "True" | "False" }?, attribute tag { text }? step = element step { attlist.step, (use | do | \include)* } attlist.step &= attribute name { text }, attribute iterations { text }?, attribute cycles { text }?, attribute max_async { text }?, attribute depend { text }?, attribute work_dir { text }?, attribute active { text }?, attribute suffix { text }?, attribute export { "true" | "false" | "True" | "False" }?, attribute shared { text }?, attribute do_log_file { "true" | "false" | "True" | "False" | text }?, attribute procs { "int" }?, attribute tag { text }? analyzer = element analyzer { attlist.analyzer, (use | analyse | \include)* } attlist.analyzer &= attribute name { text }, attribute tag { text }? analyser = element analyser { attlist.analyser, (use | analyse | \include)* } attlist.analyser &= attribute name { text }, attribute reduce { "true" | "false" | "True" | "False" }?, attribute tag { text }? use = element use { attlist.use, text } attlist.use &= attribute from { text }?, attribute tag { text }? do = element do { attlist.do, any } attlist.do &= attribute done_file { text }?, attribute error_file { text }?, attribute break_file { text }?, attribute active { text }?, attribute shared { "true" | "false" | "True" | "False" }?, attribute stdout { text }?, attribute stderr { text }?, attribute work_dir { text }?, attribute tag { text }? analyse = element analyse { attlist.analyse, (file | \include)* } attlist.analyse &= attribute step { text }, attribute tag { text }? result = element result { attlist.result, (use | table |database | syslog | \include)* } attlist.result &= attribute result_dir { text }?, attribute tag { text }? table = element table { attlist.table, (column | \include)* } attlist.table &= attribute name { text }, attribute style { "csv" | "pretty" | "aligned" }?, attribute separator { text }?, attribute transpose { "true" | "false" | "True" | "False" }?, attribute sort { text }?, attribute filter { text }?, attribute tag { text }? database = element database { attlist.database, (key | \include)* } attlist.database &= attribute name { text }, attribute primekeys { text }?, attribute file { text }?, attribute filter { text }?, attribute tag { text }? syslog = element syslog { attlist.syslog, (key | \include)* } attlist.syslog &= attribute name { text }, attribute address { text }?, attribute host { text }?, attribute port { text }?, attribute format { text }?, attribute sort { text }?, attribute filter { text }?, attribute tag { text }? column = element column { attlist.column, text } attlist.column &= attribute colw { text }?, attribute format { text }?, attribute title { text }?, attribute tag { text }? key = element key { attlist.key, text } attlist.key &= attribute format { text }?, attribute title { text }?, attribute tag { text }? file = element file { attlist.file, text } attlist.file &= attribute tag { text }?, attribute use { text }? \include = element include { attlist.include, empty } attlist.include &= attribute from { text }, attribute path { text }?, attribute tag { text }? start = jube any = (element * { attribute * { text }*, any } | text)* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/contrib/schema/jube.xsd0000664000174700017470000004227114603772010021003 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/docs/0000775000174700017470000000000014603772011015361 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/docs/JUBE.pdf0000664000174700017470000151561414603772010016615 0ustar00gitlab-runnergitlab-runner%PDF-1.5 % 1 0 obj << /Length 843 /Filter /FlateDecode >> stream xmUMo0WxNWH Z&T~3ڮzy87?nkNehܤ=77U\;?:׺v==onU;O^uu#½O ۍ=٘a?kLy6F/7}̽][H<Sicݾk^90jYVH^v}0<rL ͯ_/CkBnyWTHkuqö{s\녚"p]ϞќKյ u/A )`JbD>`2$`TY'`(ZqBJŌ )Ǩ%553<,(hlwB60aG+LgıcW c rn q9Mܗ8% CMq.5ShrAI皎\Sȩ ]8 `Y7ь1Oyezl,d mYĸSSJf-1i:C&e c4R$D& &+übLaj by+bYBg YJYYr֟bx(rGT̛`F+٭L ,C9?d+͊11ӊĊ׊T_~+Cg!o!_??/?㫄Y ?^B\jUP{xᇻL^U}9pQq0O}c}3tȢ}Ə!VOu˷ endstream endobj 3 0 obj << /Type /ObjStm /N 100 /First 826 /Length 1423 /Filter /FlateDecode >> stream xXMoFW1] Gm" #ɒ`reM}RC.WCZR$w͛ٷCj!FJ(ࠄS+B9a*cPQXZXgF+p. yȜ Dnj.&{d]j30 p v'L6D,q=贀oUy*'pa/j+< Jܯ$SQ6 Nh#q=b14Pf.RQث+X`/ "EGoȰFV-HѸ$7nUJ07`1DGP*G `M p@rb"1V"DX%>V7X*0 mjGRv'#Q6fdIEi8!fa@q:ÔHbrNBH ^!cpj_X)N#p(%'4PMTa9/AB꼤:Y^6C!{(D['Tn{ckqq).~\ZŋN7Kŕ<~ՕgniҰaMÄ tw޾uN> hxOÕ2ޥuBwVӰ:+]>}47kfpRHg28_O4^sV' SgiurWM5m:OթӔVu^r"eeX{z"O$3télxD#Fu䑆~,ԷXSoɟer7O>WB*.uorx"Uy)_wsv0ی%hZ+ ܝ&'RʛXO%9IGEt.!H4}(Y =;SmO/oLf9_{7(/lѲA%dbsK]a]s*;2>M}H6檐yf7]ȑgj߄9]ZӢ{;[d tu\j4/dJ7LC|dg$zn'2#-ӼWcVe^ӕ3N_ShDjj ۔:6>K6f\39^&\znH \go?o!26WC_R9O0WVle66kV~.{o\̒2(ZX=^tkdqO)~|0&pgVO{S+`l\\Ntgn+ZeDtqdp>;.'_uYJ\Oh>d(ed*QUHZ(ŷJvi#X7>և /< endstream endobj 231 0 obj << /Length 586 /Filter /FlateDecode >> stream xmTˎ0+$$0  a#A%߯jD岻fc;Z̫MfG} q]/ޭmޯo⣩0Z^x]fkn{E+{*ʧypg6;5PVpH8$hmڢ*߄zR:")󨺠3qXysO'H)-"}[˺s 3 4{pYdrK+ a }ѫW{ Fvm7344AGc ڤ_86 endstream endobj 232 0 obj << /Length 770 /Filter /FlateDecode >> stream xmUn0E"y$U6ɢ5h)8",c\Ws/.7?3oz(yѧ2zvAwG݌=yzVmMמMW\=j_I*Cn_f &1y+Sw$F5? S4!1!r3Ҵ>Za?ɻ=ñK}:j=w(]UU#5dkuѥy e*x12+Sx,099)5tJN'{fS 2R̼  KV iXBRs>^ .KCc2c4&Wo"q8^zl p5u%=cK(q/?xQcc/s/G|-mƯP/S8+8 4fRSYZ"?.01шŕ[KPKS60e;U}Z8~Sg; _gvi;Kc g̭oZ ' L^ ^$K{)p/EX{)^ (½ߎ> stream xmVMo8WhCj~H\HrhSbd IJ!ۇռâ؃޼!9_?7?UepPgww͡pcӷx6׏;[Rd񟇧}z eq<÷LUJM롯{Ni~l1>_\}~8ȳ&qq;RUl, g^Cs=~k*[4^͖OmTI:/nY㵞1Ls*J`#l neܢ8Wi+xA= pMn?SbZbh`-؁6+ҖtΘ 7 XB[M98h򯠛& jwJ7ɿq/1n^i 1z1MN F_ HĒ?K|M,愆f[ eR SxK¿ec QR+ey h_8khG_=soSs9S[<9^r%Z:k`N<'{>[AkZ&# 9%F-܂ϩ=WC'}k_KRV³ᯌQV $!6n/xzjgu endstream endobj 234 0 obj << /Length 1026 /Filter /FlateDecode >> stream xmKo0 ޡ@wbKE=îv;pCL2bzn>|ܘnxv%p[)OM5ף/ߝ\qh%-p< ~۷k'}r6?F<.oƓOVn<k~I1=9;[ˡy6Rw2)]~C2Dww<_ws1vn<ďqǝ{r?x),9|?\LR`йiߺq߿.I㻦\}𥹢9/85dNrf=KʳXxΈ9&^zz_/e%^I%Юskfy*x7`?J#+ ruAι.Ț낼 \duA\r \WyUb^卼:oy#yuȫF^7꼑Wl8/a9/Qr^8⼐Wyޅlf`;%[mp$[MyX[R+IL6`Yː 9HKvvI6)+Kk ㇹ7+/Qe\G$@if<`F[fĩW诉70O*Ƴx"ÜE)=+b~sN~v?SȆG?r#W?r#7?p>Sfcʥ~dFbw4ψ}}kfGl-?r\q# ?zSf fWKfUM}k5sBoh:0Ν4}{CUNzVcC6&9&jQ,^ktfj)B5&^SkP{MkMC"^+C*^kP{BEքkm V:^LZ"R[=nj lp\u[#CWCi8,ߙ~4?s endstream endobj 236 0 obj << /Length 314 /Filter /FlateDecode >> stream x}MO1+減0n>.HX_Dy>`ѯ=p%@Ugs%g@KW ~ ?> /Length 2873 /Filter /FlateDecode >> stream xYIl )Nh3\EM22#%q' y(Jy>21ȯ9BϜ[G R^MW9??1<bճ_UYלϔ_7~a곏,Sǟ:ߏ ކjP!LŐ_!&t*Kb\r0}wL*c*@bL8*|S2WS v{! $ QnO}$G1B6Kź{.7 8%Grf9OnAIQ P-rbJ~W.|Xb~)L,]p!aR;(O<KHbA8٘P=iLfHCLeN[}EI1Q3s{Gs@9q.}h$F3TlXN23B$R7S$e\.hF2"T]%a7@e\_3H6ql!atUަBVJA\<%`4k,: #O;颏@򫾒4'"sy1Nۉl+F%3 0@9^sE78V+C@!"rBJQ&ga4#)W_cW+9!!;UlpHԙuӲX+(fAM6WP≈ZfXd5 %qHwg0(֗9wgv&@B+RGz:Tʯz ]\b*ߑ< zǕ}PCFPn^x֎-3-NJ8~(7* *9 iҁһ7k=":eַ:l@J̫-q!$cUQ$kBq@&!PQM|skA ZK[e@oK,~C&_7c1W EHDYHT{hW9erz9G*|?{'j_-ٿ% }I61۷Ӹb|#k#hu[ӇLӻ?['>qyFGb޳5߭s{{ޢ7HC%k9M{'LfmWBڙř#lu5Z. |ܢ)[}%] }]I:%H ~[#R~9r.ahme+bikf$ cAuE#<Ȉ@sÔK'҈`eŒ6E{ʢr+抡53>M OFyl xE;"H8Ffj>q1#4=ͷ(t1PՈXcJ7{rK{kl,Ri KsF">U$L;29]LoK{)M!X #Z=cn4 cLkvGb|'PN75 yJ'>hm3 l%{6p@EP> p,s]` xKǂ6jY]T6DB;8)ƕvxd>kKH+LTHI @쑶Ʒ5W}N6h_/ب j+> 3|ޡ^M[ȴi2 9wU|׻!Q۰aX_ Bk;õEly6N$iRHǴ%"GYbGo024vx; CKG.L>:,ھ+oV_A_P֊>,sHo!bpx.>ELڡMol>U  gFρ"Q'-he9uê>su:u'ez NW@upy wVudtg I#&>J\RnwShOۧ5N|xegvuA4bjxnD\guPΧ:*"&n.)s|CN9٦#\)O*yb&cc:CW}%͸Q8DR}µҁsImJmş{Xup`C`¥rd ]~u(W endstream endobj 244 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 289 0 obj << /Length 1445 /Filter /FlateDecode >> stream x]W6WҾXW߶/ $i0X|Wv}%$6Xm`Уͼ3 pp^-X-~y C!DaaUꜺu:z=>G ta"9x91K!@H=mc1|xBiu*pVx"nǓ@hi4+J $)@#Dc=aF3:K7upu4|$XV`@:Z^+6)y2f 2fLp\m$QJ/O6IޥZ%J%qCQd= VRpky ]Wk ݪ(^Y/UQ}Ğ+h,f57]fug~0V8`be(̋EJ7D #R*<*]e8"jto'N"H݄o'vzHU Z>Didxs#cq#s+\'bD .ٶgs/l\q"w>6wZ AA R:71ky_-R 28J+P>TV>y붨V7dc^)aZ!bPLkg[hyn*Of{1$ֶv!*S*T}Qd:B\J%tKԶ)y3n]mh8 CE')%(t\P }0q;'~C6ȶF 5w endstream endobj 310 0 obj << /Length 503 /Filter /FlateDecode >> stream xKo0|9׵Rjnmr@Yc@|zl E9UP}cfa 6k*FA_p3M!nn%GG:D0ʛtXQA.$,*rޝ`gbJ^sK9YkJ_NO8N>U|?F0fUVShƃocf1jyTMJ]{fH%b@zv1/5 Zyb9֖5*q*Sn` ;Sƅ$ֳ-ꧩ5"`&V͖{ *nkfTu%X^.z~t(vW%+Nc Ubϵ;첹s:;eys~"<_n{37E49\~6% ]Nf/c}o2`(Bx(=7ߝ䝅/J@ũ0ڷ }~i_KAW流#V endstream endobj 314 0 obj << /Length 1197 /Filter /FlateDecode >> stream xڝVM6 W(Ԫ(uLa&)Ɂ6ͯ@@^U'Ӟ(<EYz)(Xٍ=XBBlcGںӽ>W;-9>n&_+8C1i0im)vJQ~A,$}2SW2. 'g Ժѻ [%>@Zm3}3=-\-]y7^o'v`XYE%۱0 \ogTV ȚT8:]{ĕ)".dB2-OiDAg᭽ >x.(.2h/%wL#E |sBb`x&^y-EH]rIj^n@Mg*>Xc'D (}V.JҴ >uL.{tl{t3N 9Rævt4˦kGBloDh{rR{y`S000 (/Mgh7[UpG4\=jadžf6>o>X7K1ê2@Wl cI-8^OܙI`970_k?y\L$ٚf0f t PSR~^t~AaXI la. G6쳅 fh_I{ M Obj= WS{苲yv0LYgYzY|+]ܔWmOmt z+^jyg(BgzyF^=>|Evx}E! d:qrytEؾp7=CtsvMsz _?5UyqҔ?лyM)+q} endstream endobj 320 0 obj << /Length 217 /Filter /FlateDecode >> stream xڕN0EY";.JM%_7 $6,,\yc,#Y+ 8 .xT^A`KUysU6aJhQ]&O"j4AۓC8{~Ճ6> y"'g4J/?aD!daRS7)t*Yeuts{3_O endstream endobj 326 0 obj << /Length 2903 /Filter /FlateDecode >> stream x[o6B] K%R rH<^R"y%{jWJog\:,"p7#. N0elԏ'Nv26 UWH\y.NPYsѐų#PLe3©Q F#'%FĢP P2 }MDÑV_PO:L<"RDmGP,ϐ(^'bM8휅ʜ X;*dՏnr:AYS-ISװb _Vu~S&bbqLEfu+EEVǍVr'zVm>ǿ0/ kyr&Y8TƠ`k!=;cC\E"?%\u=ťig$*ƹp'\v 1dB!}_,mXՄkݕUc+vv[.lx_VVNWq#9*^eSAFCV-f*っU!L+;_  G fhھfB׍ʪxӵ`O3ؐVğpRtN~B<FSnQu,@@r>\IjM ,ǦBJH5{ kfoDJ=,r9} hJ[hGv"7VOP 0,xaU xWN`6i rjx V`uD GEibӗ 2ᆡ@n* A{\iҁ0I5C*OtR鄟oW>(dζEmpM Tzjj|m_۷ЎjF-MnSr7 TtrA1)M6)mJ&$%!?.u=>"_I~WǙ2R lLf &Yp1-KiG^swyOcG@LlAʷ-&7(I7C͗XL9!!NX-FHX0Xun/eCxz5@ pLOM^,I?sBXإƬ ?~^PEX00 wS&cNPbeb *iF0/ïh>e!lO*DS<$,dNT1h:v50RxEn\I8؏ðg^uV-!j}HNXLŕ>/P:%.yO}]32sL#ޔ m[6Gno\z.:PpzOG_#wuªb#aٺ{F ouwBڤ}>rY-_3$wX/r6HوRM@/']ri*;.. 'm] d'U{=&s&ɨasmfb^0)G,D*.%venv^HGEa!H'T'G֭ƚ cukutxs~ !B /Kmv/-Wj_Nʶb B7p xhXěs8ts/`?{xooLKvWKXͣDG_M-\O&` GvL2x3fr4bx J} |.a:ac8nD-qsk,u/gE_WVe? ( S_'pI7%UGPfEDb<; & endstream endobj 339 0 obj << /Length 3180 /Filter /FlateDecode >> stream x[Ys6~ׯa* )')ۑTNG8T eq3CNHeF)%h@__ Žw<998z^b-wr ?f߽~r<_0}[,w4 5Kȿ t&UJ4?O;8>9@.jTzro<8.ͬ(dzz\_&IΤI-nZD@F02I0i& i~nW:FxG[ȈJ'9g$3==>\ TIk 3Lq3i"fg`.fuWN>mw5 'C:e = 4w*kYUlNE^\YYU КZy !@Cy? (WY)f!3+["_^dK䂆$3{W+ ^\j>]Kۦ ^Ҹآ՗[3 Ǐ@žuR!>D4w}ǧ߿'/&E]| &r ^2~#1+ΉMꋊ.rūtQGH:C%_P2¹*&}XZ g",G w. X5fW* aG؄ѬHTJnh*)c {hW<6^S=f BZd:Ei(uRɽziIl:-ܼ/p7`~&yf&@/㧧8 0dév $zW4J5gHa gβ ƮhqJ))E7° p,6$_-*=7`ݭC{YQ0r c x֙m4>hE6gCW?? *Pr<9pn']fhŋ쇔~t"x7S[):l!hn'.LvCY 7IWK2V~Z[|Y'(Pwؠf$. pJ"!2$vj3֠Α1c=!HjxI{͓>w7F"6q ~bD"?{rE{#3eV_;KE"PGđR[YSf i'Cۦ6jfUuTq*uBꄋNB `TP}*d SP"Г9<ש: rŚ]<D::!W_PbGBWh6~ A+ ;7B?;ǖc Y#25+vğW|tC4vEaSc\bWZs'eӈ]Ss 4]~k$1˦?MS2fz X&)Ͽ8bn-lt39#OAS/W[T#۰W%u5qrћJ1 Ah[|AJW&! HϞŴxDzpv29̛bה8Dq]:7)'wkV3I+Lsm&nBFgvb!$);F}f\G`Igi[a49"h-̲m5,P[ P.Ȩ֨/XP熊-険'O&l?>_C*'P,^i6r6iRe[A.$V:LL+1yjUwĦvԂI5ȡdXL"PW6^߄~Ly 0TdY  k)..[;r0m‹G=C ~Ĉu@SLl4$%^-(θ&Da]DqɤxS&?+w߅ Xe^6wm^ߩT2 p\Z1{H0Ȅ;ԛDj\xhFjg(;32ە8#!6M-yվA NS"]2wxU;Y5ê,Ki}vqڜY;P*훣6d a~2xS _!K8ՃOSH1:72f-^¼G`ҥяC蚪 gX߹j''Tju%+=Y)۔ۖZt^7hh9JW5|BnGP7ҷVS}5ui*@E^M5Ή>X5RN+x@Y3ܮYܙ~okbx{#A'4`f{& ˍ{'PQdʶ#WA"WP dF89۫' F@ACޮKs+ .i^h[+6λnio-52-jnF(Fd(]5Q/˜OK<lyH4_+_>yz*Z6v^!>%2)P. Ӝs%slXcQtoÃp:xvvnj aVv0{;UcߍYrMCƘnDk4)" ɽ] dcO Y9_(dl? v P!ctfcE\xcE QrŃAA~I+Sh ]9|j#`VrS<Ӻ* G2hҕ{YrA@htѾ2wYk/dR6/yn}PV0dBN;"rE%Bq8]Md!Vc6B endstream endobj 204 0 obj << /Type /ObjStm /N 100 /First 882 /Length 2566 /Filter /FlateDecode >> stream xڽZQo7~ׯ[;C0 $u:B2dH}(%l1{Ro!gKE6)OdOXB2>@F$r5R41E{cjA(ň*xc|F6"N#a _2TuS`xy=9c h"'bZ7IxQyK6٩Oٓq&3h&*ٛ,*8c< "S lp ck1DZp"P*~(4XJî4cspuajD .1r:G\L**mf})VoZL/draGdu8~=xIKszYxﲾ8'J:B/rzo]WI{_n5ys7hr|5_Zdbk^>ݪ?lZOrլ㎌#6g 9HclTC,"D ;z܃9|B6* F @2s&zN#E)OV@#%p%BaawJ5dh<'!DQiUS&V;;Zfzn;";Pw5ַGsy TZVyŽi9꺝ct!nh'IiVuNagӋ?ϟo{ӧ-(W SF&_!Q&սoVTUΗ =5-/?[U*LuJѵj1gi"]#ҍM5ݮ؜CniSMp z n"|n-[P)+x*Vjp6f=:CW?9ڕ endstream endobj 346 0 obj << /Length 3552 /Filter /FlateDecode >> stream xێ}B<6IHnIؚumɱLAP%sd6kx.\(c&z}I-2JTFWB$r)vг-ֿ-E401W'>8Q"TtD? ֿb&46Ҝx䛓ءPeIe%LP1ڲ+@Kdz]vW̅RƭRkb2.ʢ":/j2Ád:5Hys{3上ihD1ð$ij١tʔPgJg gRI,h̴b ލOl.(7:u< rdKCJ7' DwI'e /Kjx H @W ܋!yĖ%B@c Dt)M{+MXFX/VHWn?^{9x h)DX^+aA?,M 6Sɪ~9>p( 7)9K=+^|X-a FeՊ&gCY+I`AŴr#dۇrL==utu`.u:+&߽&kËH*ܰL߿MX 'XVsg}Kc4QwHˮzŮᥒt/4~Uǂ~vΒ G(Oكw1~)<`;v)8{8qrirWV<&&q̎& Ӡ l6l:*o7|S9𱕹w3oB_4#*zn&iѺ7}5E!_9ɧLoGSʅJf .X]]gCm`>3\좦{uC|://;Kы,\6߁nŮ)n#݌L:8Ēg 0IS7Y*Fʔ>?C8J =x`d^wS% v+ߎ'3KɄ四Lgo>Nd7@w7doSSI5R`**~UP}-b! i8Lin̊;gde'fV٨B FV -S߻V)ƃ鴨+.hv $WH=\5*o/ 0+[ᜮW[ $,A:g%/D<&542䮁l{=j*I!f'F >ILLe5mh[AykL LzYɻ4y/+g벢fyUB^g=]];by7W}Uo°J̀Y2{}Y6tcQpmo'UUo&Xrcs++TmU hAl:4miS69lc 15fB 0_5LZiyR`(eKbs0QZ̊,R-%yYUqp@h[nh]t;2ۓA9%W=B0Z@H@28nzC\D³e$MH%ֺx0)t46љWâY*ըë!°a0k 8{[7SX6z;ne[e;U^$6z[1Mm+Z8ie$]uX.+vL FЯiy+0<Ų_g1aDr*k+Jd1SM<8=΁ .ĸ R6>bбShͫӄe>| ֗N;/5bDh lrHTh Mt8u8P6MLw ^N, @W|9]ÛN&; ӚXΚLFrdՍ>mIjCy>vHXmguQ 2_7U=NUF#A›EH]/,gHx sG颯o?ɞaK.=ȥaC.=ǥA;go;F=6q1 zJD>#ڌwIaZ~I}+rB!%<]@smC׌Ǿ9j}`e>Lx ȚAŋ27K<Ք9ܬ {'bXd70j&n@a)HUXW偼eb0N<#p`Cd>I]uqE\>E,ͬo`F0| O,Ĵ=H/-@{߂82CX of{o]sx7څ(-u5]M3S4mm&ɑ6XQ RfYiWDP( }][_RT0s[6v B]xTʱlu@)}1e0j --u). nQ{N8hS)9{{$a_h P^|>TV*PTn=0g_ݛHXkS;dmKܙI}S-Zsq LzL@c8s*`hAqâIg @;gԦGeUR:启_$2 m\9 _{r'0v9A)C3bQCa2x1V p,ECq !2>i3]̸фUuX7aaҰ4Ԡ =Fu#q0ޅZ2/ tE T[ 6F;*]u_]g_R̬ʧǕta.eB^µI3,H/ Q3x/em)\Wl?9f57/p : !Uth .CIh'U(;!KPjr̢5ǵ1  ]]Q"(k4)U~yaZΕGA6.g@&OYAg#+,h'-q%#g|wPΤ ^:0@w!e-G6> stream xZm۸B IQ$@\\ztn!Z,9z͢!zvۦ ,qD<3qޕGg/^ /$һz{*׾w>,+KqV-W"<{qGe N$_.ޝ8| j$ޟ}z(C]^{Op%ܥÎ$!($,A <),¦֛<{_g3XXx/ Y;_-3B!f*{< ΋)جPm Yf_p.St(' 賗&YyuTe\yW֡^/؋Ġ)!cRø1:M6\!GcZYŇ rFE@M3J\nkJK=lndbr' mWүq)fۼGX3 ɶ.`醶.m+糧ϗ$MOW34>>,[z/̔Pbl¹g:ʢl9T+lUp7 9O7ffBGL4:BG [M+^A(Ó26p.IqmXuE#RwKaGVm#cBE!׻SrmaA),>'ۋ;7ٺ]M.\PLevӄK)b̎$791vo => &&$aJ;WK̶}&S&1΃Vvy"lW֮+qe1UoP/i !ZV/%˕  ٕ7tp82<;Mц L9#aDzΒKǩ M|[XJt]|ލz0Vt€$N\U]<MVfm"SJ{fFS `@=h b}jb;kbFY뭬sysK&5u=^\U[Z &Ӿ\Ҿ敏0qd lqeg.bcJxcěIN̦P_ώg3fc L0[PL٦q}UFahǩ^άzX8U)Ž#\ǣ&Y ~7̄Hl$I]6Ř'O73; mR=tlq`Rg |Q qeWͷﮫ˵u=YFAF}h5̬m٪(ɦf 1#y^f~ylIchRw# ݻza psH c02wai>r r c |D; endstream endobj 357 0 obj << /Length 2616 /Filter /FlateDecode >> stream x[o_!/6RqE=(n/}("+;Pԇ۬׷H̐)#}RjH(AI"]w2&YۢjZdmYWm)'.\]r`(p*JMo/޽ (i=VH*F~zVÓ0Y*1IJgYPaP0NAwtbs7wЪW+bTB9G&&M^WmVV TEnjMYv vr;E٦l\-J% 䄙Q'D3X!v._"hZ1$fb^Q$q,UB$ږJ/ KA 4Hz,1CU4CaӍjhOPКNw Mc (UvaU.$LjͥqJ>h˗QYݏkKh dXӇwPy**8fv*uJ($0 iΗ~_K?ʨfE-﯋OK s)ͷռ ox~(r/-JRMY-|\JXx(7RRU+Cp־a)t?pc7n3hTŴ4umz)bS7M[v iKbzw]mŰKOU ZL6߃mj>Pew~,$԰9 m>Ώ`fVX&1Sݸ"0 uIsX]u1E,,˰_԰a5Sj$^+C}Xa5_a[ˀvae4MX)%#-8aRf\+OYU5zKgVescS6-7,lɢЭCClyx#lsMi7캼/t>6R[n3~94N}q|z~uI#̷!. &'xRBpl·'xLx" swu_m6LJ3Ze`QcL64L !&L=b`$av?Mq"R$o(v>;?ԡJxW{ mC}Ֆ"y帮9:Nܝo`DwDw:sx8ߔRO\69,%0mA\!?SnBwg3m_ʪ-.k;N8a|H4)l`P|j¤|2,}ٔFPhP=v_nݜ4E}<:|.Wc?DvRb&f>$6Bd6(b@6WоtWlsa-"{e`lDUV8Lq(ok> stream x\Ys8~ UYNcjdv3YOCfʥ# I-KM{gSpt7 |'^<{øȧwq y ߾OhuM$KuxOX'(v`V9Q'z(UA4}lQɚ>ra']qV1hƀREgN1#WRY^8(6ӹ\䬎PMh"A5Or-314&۬by=fVXfN}s<ʶ:15&M&rleޕnsbv%)~o4BG@p(Fu. 8 8塯Ef2H4:hP=D1!cV-}iQC7a!h((bf_fED*e4jlfq65m%i,b#kҖ5p^QR@ 2k QFhJnei Mi&\h THesfnvj6@r` @aQHBGFWu1'9_{&~4q"N/S\Dd0oJQFP?@4DF)dm7vԪ­Fq9TUWO(T$ j-HV (l`FxZj&f@Iw !B&t,()J YrG̟pSe Bȑ!@Bp39CPVch7͘Wx4/&08`_OuVγE~xyśIx]А91MO) *:$H\:9f:>&3Cq ji܁׈p_@4a7eEt@XS9/oo*AY$(f-7rُ^%|P(*|! egkD' ќ bև $hxfvc3¾*:BzfBd]wYz|^gM&`xAE!j=K v*d(?Y\*2;q|y:3/1al ͎8Ct Ayӓ$Ls^)\}䜞ފ8ZWnqW>%uA;DD.O ]RNeݵv-^djW:\ǣ$ P(Aw$ $ٮ?6QQd #[Qƛ!# XY*/r > "DDAY݀~vò2 ! ~xxX]`%vJw:Oqu,j}>EX&Xš ܂e cEI!aYh)rPe9Q9Nzr^=AǦOp75š N]@[Mޛonoa] 8oGv> stream x\Y6~_A`SS_OK#qfە jFrv3#$NnO')J%MCTD1͒q~ҽ4 |>{&pEW/~pA`)DQ#d4x;N::hGM.JI 4rTgc("QD!,tvd>,\T""h9 Y˕1"0#*Ud ?ltɒpĸfa 2pWIƽ7E_8[dq6*>' O7p9g}{iުOD.nG½gg$33tr֣b̌p\]&؇3v1ww>ý?ZpW/>QY6sf,[rAk#K?҉9ג4EXD' !F[%NR)/{j]pɀPia${^ :E~ v. 2fӺߝZr!c%0rh詺J'#%1L|d5w}%\܊JC9Vn8e+]/`/fVbVKj1e/бX >n)e^<4֫l.6 :*uּx=sU>Z%쓝t,/i#6a(ԳI#6b]4n!IӤ}NZh I]\oW? /6@MO,ZI A&qt:S…S<$?;}Q ;NT2da{K?6q{rXgG.lFGe ,b1,QkE~]lN [z SpM iuL8r/5 TUxy0g^? >w 7 l#>סz,p@ llg3"D`M~^at rXsٰ.`>ݶ Zg:ۘw^A Gxw;¯|6n rp&pl)oM*Qh4,ƟZ;uѽfRuP.QJaEOآ$r8bWЄ,$;$Q4S3ukn8m?ۑ~sRbg@ C7%7wgުUUnL˞9 OO4@!EH? ^:\Dp@]QBmEH@q.w$2'wP=-Ƥ)sULVr '0A-օ`l -g;<҅x!F#⌑ZmT (TDn"(ƒ`QP d]?;@2%i;-Hز4 .| lp/<0 ]1~z {z.TyZONQ-\T|y!3^|U#Z*Šf9 o 9nVڷ-w yh#!W 8:sA‘'O=iGP!xR; {Q;~dbJ;DU )"ʝ*mH[`9@LciInC?޺EaW9t5jʂ-jLiS4R{ߪ A{uS o [ i?2XcEѷ(n+1mn*U {9AVG]Y3{ }ys2߇9#-/ H)Wo LhM=/2?m~(>d06_p_.aoH8+ӘvG endstream endobj 375 0 obj << /Length 2949 /Filter /FlateDecode >> stream x\[s6~`}f#$wvn^d3HBRu-Ǻ8RD9Εqpɓg, * AĈ,8G/^=y:(V Y4 M^K9i-̀"'NP O [~̿0bI\U H<q\rp9o0 \sXx4]5$˫l5O+3꯮u>L9V.cGOc[ZKYdݘqn *Y^L*wo@ P¬ (GYXYH31͵BBMD0_ 5riFi13'ySe,0\ʒ AfE"XoN JU<1s2/7*2R'N% ˲'ڲ%#O+̂.8&I\:jifaa epW/+෰1(V>2Q(Y4fY[(QkkyB 5 XhoYkn2B@:Jz$IlutSty{G7;z_[oX˖$~> WL;L>!jF`bZ]wڲcKnlF qdHчs>"MU~xlU8 f=A1(p֪#}hV ouWusV7r Bx2ۈ=+#lݐy cDlMTkWVjP^t7.?"з،=[e6LYҩF"+:Mv];N@6SK_Bt0F˴J^_ JJ}WPBKR96X5) wX6IuiNbE21Tm] 0!!S'z!]`\_& 7lH/yBPGcX٭X[M)7Z 7A1'h Gcvh%>m11O(oh,l#7'Y:~2T޸\7q7!Jy̬,5ƧISsppõT {pAjaCsܛ.ܷt$ $1AZO !OPY!(d&22`ej(D6SIMޘۿ )~,1 elR8@A՗+;]S*IF?f,H1H!P+CfpVH93tPkT4VӃNe l gנ @_K`]`1/WUϓ>'c&0Ǻ\صYS0uV:R&zPRj̝x69US,<,/Y5:l@qQ 6@~j@ aC&UL39 sٶvLRuړtJlQl&R}^^ Cl. 8UՖB&I'8M1^[v*Htɤ+sD@^)pMff.V~[~U1󄻝RBRWQ.r|*׼HrQ=VNؗeѴ]S,HwA5a(QnYmKE]qf05S\zʜ qGd)JkFDhEszE@/rk(8"UnB~`b1[5x3Ɨ3~PS_eU}|J>i*ay|$>ۮcsbI{і|RŐJBn>j*$-8e[v6Z馠EZ /b]صތIr= b. r O=w}E%r,j7vUtk< 7QM-uPtXS\F1y`ӏwrkry*54l|&.Q=7Ih;UWш93CL&& TJM/ZMM&̒d`l吾=bRov1C"J{rCl_QLb*dUd^-Hɝ(s&yD\ %+'uX ͞kJvoz5= [\ ~B6lu#x I̕>d] DAr _=upgH۠ =~gM "n""A 0F$:eԋbWń=%d&~ dbkRtLK[=|S|mp5=:/ͼg)y&K #%\rk{(P΅a{0 W/ɪT?1-fV+=a'ه*r2?) ?<}ALk&mO77 oh[$D,X|oƷxТJ^ʏ?|hC+`xI7 QxOY8f:cAՆC$ W>6 v#ghڡA=HRTyޞ 5ndÄǠf&`*zph$'|c$=@pM ۻ"cHwwC{Br<ʇF uz/cvő~Y٬<,YJ/fzxPaK Ǝ`ݻ*X/ W}Gƾ횚7J++6&> stream xے۶}4`\ "M2cvRONC.e-Hv=+koc? G9zvrB*qtr1,*A,aUz:NPLD <-w8A7[F#F2IbE8q_b^Sa.f׶gZ Cuu'@.HHS.-oQ}9iAv+ <(nn#Z؈;-w # 0̓ޞaj`UC\wxɋXD0${~@]$\VvXtzbOs7kAbX`ڜ  hB;S"&M`,Io>(.!~gBWobڜt3;E&6RJ)pG،XDR A-owуK%X@c`.ԵZge?7bE%4~X%wW-Ffy:lx smne. VZρ(f=ߌt>d8!c~MqG3 kxԻ^mO'nsp2T)ۦ4)Y̺&۳I:nxĄ>{, A ND|:؛;ң|2%wLmJc*KةR du 8Q|xbMI2ۧnqȋ]]znNAPPnvj[HsN)m" ËO_~[ N J?[Unٽ*}Gg_Icݕ Q:r\xzp(ǏCPgu|`N!﬒a^*Iw C\/+>k}?qngH ot}qfw:M oulA0uTQ)+4TPJeP]= yodבU+(B`c bK3/UËzE9,Mpja-y { axQw* CY7~#k*KHv~9&S rЋES˶\*u3S-gNt9Q7IWzӱg:Ro>wϴ+|~U٭ZMnU/Un mK"@iDcJx{١~6pxhk??1EÁzc Ika!#K @ lYc'_cȢܑ z~շ y7e=0e1]gKN& U0LwcbQO\LQžn7T? zFs\Qqb/[,76[/v Dl(7k1)~̐5\S;8kե!6\Evn?JU_cb`c<| B51($4gH]3һ弨| 4cyDLu(gCrjnsck}YxGK@X 2_୫;'4Jc&u3+k@n`-Ih[w4ꑽlۻ+ދc[*=ׁaWXG4֏lN ,cϥR1Wtn`z';X >}85} endstream endobj 385 0 obj << /Length 2698 /Filter /FlateDecode >> stream xZ[6~_N`ckQ$Em m:}iZ[F\I$=7], ,sxxn ns I"W@@%1V_a*Z6YryU~k_eE6ip>W\0P LT\+P"8ӣ6A(*" :@IG(%42(fW5`, 9{'fyiy5gtnEf5YZ/Ln3;l=鬲S3jn6-&[YXk)ˢ`fg= BC i_!}%='̿I!6- yaE Ac DJ3oTE6Iu^dg@D$b܍{8A)"4V^>?!F/ 9N+WY rpZVӆ a"vĴK¿dھJDoNV#N:V6>9$B&seY;;5lU$@"6Mf;}:V)yց];( nv }k(ޭVY+.4۪\FRd ȳ3LE7rn =c$V℄';H `*qIB޴ykC $ CpX7/DSt`7ĄaHD7es氨i^ׅ}[ #X9/WJ+'jײjM n e;&Rj`l-0rg67voj 4vƻL[<ÂT$d#p녤t9[6]f}۴m:Թ'?~=(!il}3MBP 6ٻR8I9n/+먯$W;JmVf(qz10 5ΖՌ8ek}zFN.nb""&,1v `l;(fب†SQcPv7#l;'B`9T?M H%=Pؑݴ]vMuQڛv -/PPuS HrZbhH0 HC~/56B omH'\wS`Jg -ȩlD `p-'ҏ$1lF2 dD2|*Iz| M!onwYin]M[2 GTR(- FxѼniS&Խ!R()ʲ*θ~S1S X~L&P8`t2~ &k >F|Ԋ\W4,IŬ-P=LoCxl.[sm1xv,\2 %Dar5:^eT HLWN!1'd`+&-W{*Vg jQH(Wf@?FɱҴJW ޸)\{P-`Ǻ809c[_z<{\~(<#WYh unn?:]bB챿cʅ5d@nG'[B#9Y}X4X83Qi n:[ '%jرdAЧ֎m2A5/Kytej*gJR&pwaJ>/=G>_Zr%j ƣdm/J_~@EBti Bj䪱D&%+ ?'F1Pú<$Cgw5B^,//~9 6F72~R5S+ 0 :Kצ) ]qn0_M7{8gZhlW`^ٕ1 ^= WG0]gp5VM&<h r*1K-#&>aXu1v@/5uAV9& }㝐nQ:5zq-ԸmCA+9sw }v9Nӭ[N3TQt ( 7.ˠG{(:X}j<{| -Z _r|_~deRE18a]g8Ix|"Vb?M~_Ӊ7Ss0uAUt=M$x92̗ Æ5.֨22tsMPlSk+wچ;CiP민B==36lȷǼoC6[#K=z`}jp&8 ::qT1P@gߝT뫅-=.da/)r~>I|;!YCA%5P6SM!m Ϟݦ9~e|I#`!~:/۬ endstream endobj 394 0 obj << /Length 2295 /Filter /FlateDecode >> stream x\KsW`9`RSή|:%-L$&$%y@H4`+bou'?\xT&W  QSR\LÏg#ӿ-ɋ8͊"Ͽ73?H"zŻ/N~?!(DQ+GBdsBc--b \^ џ!=.P),Dpcc׋a:|xlcHv́FC3w-!pvrLF)KⲢHDZ.g\3<.Sq=r%e7aQYkĔj ʖݔ2%(Szp&!395K{fmU>aRz8}ZN s==%9ubEi2߅.\MO +"@PmkeҽgVtn|i~|d예HQ96LW4Nf|>84 ]l^OI3YݘyO >vJԳU40s(^l 1 5'tu:x`!=lvW]s6DTy#Tv]E=g`*^z}y Gf $*j*3owQ\ogl5 FsjkOFb>xo)xe5n Y-}(ZU]!.k^!-9 eJRKdY<7fj9(;;>% DEf+ }Me@,֞BL7X<溙r>ESgS\CBσSG A^4=<*CF;f>c}$O2_*_ q2W>DjgR$)oTYWni*☟vh$՞=9,p0^hh=Zyqg0$K =;0QnAۥW ˰ӢVa%Zm -cv)Hlz)Hߐ)b=g0e4J%2zod>[ا8:.=۶unZr Qsb >H3:D|Р7l "pDՈ[m]84R1E4?#ฬ +fMJ ]mi]6A8JQۈٲX8R9CHQBXȴ$IN;άlp5;g_[2jZo ,Y(tnM*ݴq}DKvdhHK^n=ء WqJ;ڿxscz+íëװź1>úy(/ &u}q8&/vqIujVC4V~O 8HKoz\M8s`_mt*dMlY*N]= ömQpDefAm8E4:IOl em3罚c+ҽebe MįR endstream endobj 400 0 obj << /Length 2609 /Filter /FlateDecode >> stream x\s_* ǓLnNTvFQȆ$h])PVBwczNjo)" "/ ^Ի>at&?ླྀ3]~L8OAoO\|:Ћa*C< ow =(T8F$p?uˉo-j}/@QcI-#(g^ |qIsΊl\YdTͳOxqMI|+K +ʫn&E{nUIKE IνOԍ9éG0 DJ&ye4 7l9Aԯ\ Yq0&Z}C_ r P :i69c|x ];r Q,ݝvVt:5.O3(dettƤzн9_! +}& S3`ra_v-t}ZRk}P`X]jIETT>]~.S~BM [M6/dDB:i"^ \γ,%x2iͳ`LC;C:z3Cnk̍ א3K"BN:ux6sU"avjQVIIN?mIu,612mo'͠ ? Qu^>aaTO9QE99]^'AuAh5Tem !msh.$S'vB%0Syw\Z 3Z"@QTErYmWEXkaì?t( D$9TD$y1ݺK]KaՔ;noGo_ ~_@("Zv9n贋gdYo;!v=Q즻|Y3?*=ToQ24̮ݍ{ LFY<_ί6f@bvEצ 1i4R0*A[$ fy2D9H8j`,r2)a @(HV o$La܊c@^mήlI0C#F-g୅0p".emdCmmsVMFvHcs\͒U+ ~4-c_Sj*8`TgZx.y3pܤ O~,J *4ҥDdV:JtFj&]>OcP?k޲w?+▲׿#]WJE;z"$#\rupE)tC@S#E!fxX\ endstream endobj 406 0 obj << /Length 4003 /Filter /FlateDecode >> stream x\o6_(5P$UR#MӇ+/+ۺk )Jˍ4wJQp8͐X\.ɗg'QzQDE*B%*ZdE\-6~Jfvt,ˡnD^m 4ξ;yrvI ]E$듗b"RE1:#p]8DLX=V2#.y+7vW?͢H1S;:])%gWȴΖMyw]ZŲ!?@so_Ocs'smfT&@Ľ?ZHԲjjxTH7xVnw!JbQ$Ta,C~!"Ɗb@UIթJ]lM9Tnv^oq2@zEAr-;]W}r벡ʚ -Q{He2`\AT(Z*%EM(wۊc"vZ+5,9D־רW/Ty/j=q}H`<]tϟ,^trS]Ɩ@}\j)VzoC"븩{mi%I>Mxbۖ@!>o)>eէ~G6p5G*x`N/>L dBgB烳tRm ] *ߞfمh `15DB5yXAM ?, U{kP[6ؓEQnso͢Ab8[!&65vgDqk& <^O)j孅.N|6[ΰ2JsS 'B&71IP0tR+etBD&;_naWS(Iwպe?%E捫v9ߕ]oIqoR۴MeGobAssP6S*3Isu7L; ?,J b*aFc_'9 1(/ *)Z[qH& ]ܒոz Cu?m0eX%Z֔XB /;]N0.2 Vx^7%H0 x4X)-:M;4Kʴ=CODZM 3r})2Q2";VhmP6o"6* SP !0 (\Q&NU,*4<V61eS#܁T b6z2H$MγIq~Mr$̛˞8c-_̗1eq:˗>ܓT|y ͗AcB K+p曶(6"JAEm5 @in?L"9恱C. }m[䨱g_G:{n4>:`f0wja~:kwIRWTm'*>}#,0őxЬ\9Ms%JSg(8^ޝr_'[lywsO瞦ݑЍsf"{/?@*—4ۇ{/2?.4 ~K9,:y` p ^j[C[BczlQBH=q~[Yl;}1~nߩcݤG̞;B>7Dg]s`fj7<lj_ҷ̒ŁGM?ݢQ;rX=4{tFpR;>DyL)<#Yv2Q3b%0wx!:3 e_tB@0ɜ(l9 endstream endobj 414 0 obj << /Length 1093 /Filter /FlateDecode >> stream xWKoFW6 C \K P)J'Zۙ!="zw.P3> ͐RS%n~'Lo]15DFXu;Ͽ5 fveC A[XfA%onH`ʤK[*ŊrTK}A ٔbd BRX+.(!S,P2 ;sيQޖXgs Sc0IυUͅg 쑓d q GՓI[Hv9U<%![\ 6_zV=X ({Ja% MO٣,ghik4*&9㬚/Q&V~Rs:J_ )0n\]]2{EW YN#z3Lw-zCX\g9/1P,B(Lo% # $LhNׇ`^=Bm콍󱗽cAeBc+`5)  ]=5w>Dۍٵu NeSdb;yフϭFܠ¶/D:ykGy ^9z9`k W3h !ٵSsx%k5m>@\.>a0qSϻqPεRUdxd<`!4n7o>6 ]{U P `0Y!?ƶ ~lj:A/ endstream endobj 419 0 obj << /Length 2466 /Filter /FlateDecode >> stream xRF4S$ıvoeAP_tncom-<4;|; c^@S"?%Q sjI4sh緝;"i'E($7\ygs_~uyp8cҼ$Rs|G}a9XnI B i-,νgsftf3:?x6yΖosNg{})W B4羠4e,sJeQϙ5Uܤ`0[U7q+;;I fn)ܭǖ0-q'40| b!b8OsIgw1 7 Y@ {sAmX>-7 hx>QR15 H+#%}H/CU`Wl F+P.Ӧ*k1%2PT*ޢy qo{зu%$` ­p"WΎt[>G<[hb7qMQS |HXj,AF{.x{ / $ e/Q.lcMh|݉`L=o;euq+G,+9EGퟛ)KFf>3դ,*F҈H!Upc#|7 XܱV3՟Qԝ}Z\0`jΏU ZݲNW=©ECuóz!q.G^5)m3N% .)H 'cF#UL:cPDAtŁÑ؃iFGGijV^5nQسl%՝4` b v 6@R;FrNBȅ6ƕsS[![js%Crg3%%Ƕ.$A$JB\jIa~ ?~(A*8`vح%0p݃ޒ<0Ha*B&^B_e`NZnn&9)'Zd:0Kual2^BA|r$Wي x(q@a(Kb@r ]F(JYbv g 1/?\P,Ӫ'dk")WYqd'?Ք'wGxݯ0 Avthyڎww ţEw4kiw\q&l-cj,ckb g02N?^"T\ChCG8 C ·PjBU7J4ܿORg@V'\BhJ`a@HEΞlyNbqqqA.)-b}?+&.tFJEB~M@[*Hã|,NR«21a!x˂#fj?j\}mxv`^n#l3?r sѧ/U }W0a'`\ ]1Z=/\XXPBLw;gmf+Ά9" A*n↎E9a߈co4oLmU,F~1./f)h5ڭ.ab%.\Kuܞ4]f6E`Ie0[I&@}:|kޮ?@S@)CE'HAe6)B[-h9/E83zN_yYa}Cn{}m*-'շָ-w:ϊfBuALGq¢`Nj,Ff8\ endstream endobj 423 0 obj << /Length 2549 /Filter /FlateDecode >> stream x\[s۶~q`u,wĝi2'=}J2#H*Qi:a*, 8`7_p>r@Y>>8$Y\;,rf\'NjpM'fa4g@}߱i6OM:I.:>Ή)GQ6r?~DiA*Lڀa_ U0& Nj@qte Xh)X b|egQtj㐃jo$8+A>76ߛ&OA,DoL-AOb3-uneI2JF5'QF (jdz~'rOCp~o\wȼ < 7Ÿ$9vy/LK###G˅!"wYf1|Ί*[N"ͼL/@f|bȍd^F- 0=Og?EEu,#4[PqsD8/䓝tdapsU3F7C_vlH9nY43oXa H5 r](XV=qR!IDmX1цr- GL ]uX[!NW1N5) re>u 7&ōe7Nt_/ [h°ЊMJjV6$Mr"^4tZrReHgu]I#pGO|9T_Ӛ̡ڡ> stream xkoF)P6&Y CD[R*JM"3˧iEй ryԻ'Bz! 5' \ "]μW>i<~0W>,l=QN_imp |~g |JBߛ.^ =JDxL'#pz|F2|!ʒs@_zD1ߢ*:ͳumz/rA3Wƕ uuSZD3).!RA9.iNp~vHɥƖT{ˆ7›6Z\H"eڸ׽.L9=`wxyZjݗ t>\+"\xI-NtH(+*V\J$ @@@h]NՏiI mA'o@*D32ZExZf+''];L|ބE)/<P!}[Kn9<"tE̓j2q'*wb`to; s:&{2^[Lr $M̏x @6Efq5P:'Bgk쩈,A(Tc3&D*ˋ#v/!/wym= BDX Z?;ʴgq^#{)d эms%Lf Q.ѐ"})4/?yͷO|4"uv=W_Iy^^%?|lMejޘAM .le'#TXz *zzOubhIlY^AG<{CY%T?eK3`r,/8]L!SsMzFuG)ȯ UB !VΰqA_@sT7eնV7K^,_a8v)ߖu: X"**pRakCm]]^+KmƒMB{.lp:ƌu"Muy!"XFe +n`VmX=5'"O<. ԶS|xQ’W!;2g`tHg/Ta(5%)p΄*q;&|N43~E3 0LGo:\Lȇ ʝ1<칵B~h Latx7λ&~*[uD$y8z3f,s,Q2Wk>rj^ Vc/8Gc3<ɑCLjpXPڥ[?W+"D",k|2 Fp)`Gj5Ft"F8GA cNXk@(n/o5%VSVF $YHf/cHjiP8Bz_o;:*Ǚ0gZ\&KƬxg .Y͜ɘ}nR$.:lfbYhj!PL,2/HgY\F.KNn c#/dVV^Vf82*Ut,C@X-qMF/.h' Fi\VDO) c#?apŸrG êٞ5 ?[i9m Aic`|O8>,tMɻUpipĽi++p{!"[?$sv?/P.;{@m*!UON#veٴ}~+2d\}-La$KMߙn}"II&%O&D:Cc#0Q4Ozu,#i #80h>NtP'ӟ8Mv<84]LJ  z4Y*Lh "z&O4Ol'>/S;|ޟ1_Y/ҪS^I J6MG2/3YexٵW6v~gcV d)%ʷT/oB~!9`SU;IB_a L{ e13#Vj~(Uq36]X5j4X][_4 endstream endobj 436 0 obj << /Length 2596 /Filter /FlateDecode >> stream x\[s6~z2nNl'MNK> -R1w%R\Iɖ%+RE$AA]@ξ8{B &:!8""Ex9ˊz8R-M'/Ϟ_yƠ0۪$*4He@5 bΧ_ϨC鏄HJfnZ$RF0!B+ ܿHQ:x=^y*a5d -M`#+ng4Y@ ԥ;^e i=~e-~+ :u̡/ۇA24 -:C蠨\r-JPp`qR=e^d)$/|p}W(D0bYp@ʢN#Ġ۾!a-EY >eqݱ7+쉫V1CyXb0Vh?SX*3;V Xx kX Nng"adF싒}7iXHbXt nr(y!%̞dA5]:}TXpwʉ=;ILOSeMUQ4xQګ@ʾ_BbY36 D)9ObO`|gImaeWji!^bx}wwHw 0TCFƶñAFp(,F YuĂ(;q3)T2VY1 z 66td&t}nI@/whQ"Wyti;+F[^$-wdy͞ oAW*Z/+ݮ5E8H4GUt _j𔕡l61ֱ6S&vgLpj?#.i+tU>{Ak <+P@%)t5-/ *]:]vF0uvY&߬+_BN:E9'Кq%yVYail>ިGhIUsL(=36 <_m7R0)|M71qǀ"ta[@.7ff"YrgUcUR[w9:&ѫ+EC"{%1 IH.L1dMs=; b)%]A]cce`Gptg%Cܮ؉.%ъ18! nÅ#A4ǹrwl0Ҡ:plI3TB) )J[g)* 5DSb'y9nf<Ha ? O(+e o/FYW0G='G@IpK,z]^fݬMƒ#v) 1l~L󝒶qw%hj,q \5v׮n`/A^ۛ8J?f }],נnH&'* ,o0}Uf2xtv)zzdζ#}r1k89lddQK*jSft˿uYge1-Mh4⾶ w@$P7ߙ}"pbNx[a>Hģu}{L)2՟{b̋{*㻑ᢩH{|zO¸S)i $RQ7`t,Q gmy4EjG1EW̛;Ks$潩%c|9r訴K+~ݗL+V o.gDIӮ}r*8<_۞KGQDbw#'{ :\.̦5i V<~)GS.CeW{Ww.#:b ƹq׿vOqL0ZD[E}w'0cnD]Y}ȧ.;lv'OOݓ>vW}YqUp$5 k݂<:ȳ{Ҥ{m a.tf8"O>0'diw `\% h B~]NɹϮ[L!c x?xpur~a!$-yXs¢&V~fǸ5~b_h@Su y "m/sEwo ]=$)3@uF͊YYy-n ~χ7% ;?] #Aͳ~؍!pe> stream x\K8ׯTv ޲'z7b}gg ] iI60\-,+3)qWwW1D(TwÀ `BB wnM_NiNi9'"1IDݽ+CS9 `X&Q%|rbd()Lr9@>HeH OhL@<393J_^%M"wEt:=,MnGAwtL,ّ;S H'.3v}o2\JB9ia(vk़(H;m#aroѲʥW%dI#a҅m D!]nR 76*&|Y.:<<%GK%'}1\Aq-3iuuAXޞ(\?ђ#פo_t$`A$Ըׯ*uM-Gv.w6Y}Qoغ y "۳ {Qkks!sу7~4Oq:´~VؗC[8rY*E+<؛yA%7pyǚ8N>c?LtL`7 H' Y$|*9{A|]1oI ?=вhv ~1!'RrG2w^I/عҵE vz_藛6oqrw06,H"%A?f?ϖCc~&]&:f>fGT1$:滢#^C<΁{`ԥh?6 endstream endobj 446 0 obj << /Length 2761 /Filter /FlateDecode >> stream x[YoF~ׯ`0xZ}Ʌ ٍaq'J.-`VFH#XeUIV+KMݦT.-%U-y-"T^[W(yBR˵Ǭ-|$}\S'.B |ġ11l ].^7/X;5qP p Z5$)Z[%ѡy?YWk]!g:7^Q7׃fX9=㪨UNϘ]qazw(k_"(d*4(҇2.HI'(i7@Qx +""Jh":]&C%8lˢuM CAzZm`[.bUU\de\Oox=exӭNs)a"h+xs'7q'>8-n_V6Dc;ЍPSݡ'!/E\֩Z`U oEWO<\bd0ۃr?M$# a$ǵ[|hzuH7OC`V+S0+Wwf,O||2,6uՄTV8t/PdAG1|1%ˮێ.SZ1Žn'|U< O[<̚>r PP:]lKm}MqL!jx}TR!Ǚx0%!j,,9Mk3>ECVX֟S;h̷#./7Vm(xҙ89ŗyNXt-_su6as)lHΝ&l}|0*uM.GY/dn/(VX4M D@ىkv⚝inCmvꚝfZjV,:K4WfgUCd0s H"طłQm*Y-`00 0 (1sN!e(#7"*\N 3.SdUK%ŝA͏il2l|L]MU3%|cmed"6ɦLINT+T۽j[)cfz}3:*cHuٴUj 6aD {4 3:D";8k[?i"륉XYX*A+9&YF^ \/C6 3oLXlWs_D@rAJ4dR=K- Ibr0S!qǸPf'DA:ڹ%AW/:[D[UjKw3BauD`O;W@vHl91I`j!$ssF[ oJ=!G'0*sπJU%Ty|3O{Nu3ୈV]>u)zNS ԆbjCjC9a0a8!Ӌ'unxy_ Ss˻8&b>xO/] //zj3b&(Om=A:Y*z7ۜ²njP:_0]ov/|BOle泥gU]MHW# endstream endobj 450 0 obj << /Length 2147 /Filter /FlateDecode >> stream x\[o6~ϯВ=8@. t6{j@ě-g&w(R(+%r9߹޵wG?e P ί<&y~S;z{>U Dqz0Mc4YdN(~=G^=ʑ|o09{Ca}ϯx\D} cч#lIQcI%F/X4NGq ,{Wtbn蔊޷4ٞZDp)*xU{诏4R!I93͹Z*Ǭ0$T..K}E`!qFKf\>퍶+{qBK^7241M(~`ϹP΄=7<'D lJ~X+aMs{pgXKiPO/-~;=Ob@ kJY63>RBDW?`Q@^}vt"~e/$ S=`sZǐWWsCCuR%9HV`ѓHAü !'Oׯ$xli?X$> |C#s+S]FeױW*xmQ[ޚ#M ~-)~?]VԬPn4/{m,(L   A* c>4mߓQ]̦"&Q2Z fL-11G/h{mf] py֫3Mp R-ks.hpcg7l<4Ǘ hrיyJgVWkorɥ' re[Gwe(DajіDll=I^ąSh߅IZϠ|㖟|4O$O?rv:>1zWҽuT&Qo驂 S4jc[n0gvFBj \wOp.f Bs7\瀋A>A\]hli0~~\RG. :& vsG^m=q$)%p)K@'ԹtzMBLCL3͗ɘ:2mV:Ţ)[+$LZs 9nv4K!YYc&e37Iq(Uv`:kpXl 3Ţ*"}kk29]GwNq5n)6 e %dIucQ@R E+^j@HmAdz˅kEQCu|onҽv(m(XQwߏ:7ȓNS` ouyn!J]7n,(w)?orI ItvCXn}bڑX.Ru 5s u/tHYkFdE^ɃGtɝ(W&K;#g~7h9[OZ0ۂoZvWi;5rV{+f!\"r;Fp_:_ Ոlqmgk u:Z Jʬ=l/.شqtD.]V]#8ѧk8̺⋠^,5_ѱ)^ 9[ Jar endstream endobj 454 0 obj << /Length 2725 /Filter /FlateDecode >> stream x[r8}W0֋TdM*ddv)-Ѷf)R$6.,E5~&.@bΫÃ;>]:KǣK$scc͇'>dE2ZL8E7&Iaih98|> v#{hzOAsM. ϑsvZ⚶a唂\ʟ$&"Zw/02<ߚ!rO[!0¦ K&s&c%i}G_56烞#_`Aa5 }1O(F*e`!.Lޚts`BL(PTSA?/`f”ͲUD'F@;B斍h1B*VCbQc{ᵍi4?oiċE8?RB*إ$] ܀Us9j=A6n],Axnm& 4;#K0uƒ"6dtJ kJժ$4AU ӻLhc n\ঋ#Z8{pu|7|zXUvrO-6'? lBWA{5駛ڪL1.4`;˦an^su)} ̄NTUi4,4ٵ}qM(1y+#Nlo'6%`S>: BUz+,D*fU$BphWa^6x',&Q2E5,gJe-/?k.]Nمsj<^3΢KrǹB7˯ Ќ=y s\P-u wУuD("0ux\bwAZ ]ԅEm+D~]`kiLq{L%q#]$] :-}G+(muȺD, QaHD!LWI)fUߠ߻t*tϫ_ĶI*;5o<7?+M4if&[zqKO@?$ Dc Ba4_ ܵ8!l yaO g"<:M]Zy1u?p nt)oL:3k{e;<ed&d{ݤQ uWײ!_v1L2UR+J;۬xIFդll p(|܇e' ; 'ǖ'kGt >9GAzm^ۊCE]~bK}xn8?߽894:~;;28WʲSkEd>uʷ]h~;'oO>pb%Vw?}Nbǯߞx189;[_1oMn4dA2+BKv:F4 Y=mH 5vzIApdshu>o+7O[h,YW4 vVsi-Q;,P,m 8U#),V$,^6Zڨ_.|d&=X`A(!eHRJeQasV]!HJH+]\4Ye9]tWC&d`$w*$WRBdEEWĕ6>(,GTa׫SI U|DIV9UW/]$C(Og/_6럳kq6"lL6nu4a+wUV\2X-ðbE#7_]%<, LU|=SJ}&nѻ{QOܸowr] lL9ۘHK9^17SLnRqn|ߥv # " Doְ ZO}K$\-9лݨؚ[V`--v\=(uܹjAX!WsXjPoل>tqS.wqa4v*f7 ?>ԾAPֱuH>zk)u1VV,.@Rv階i75ՃU@5aB)KĞ2[im~+8Z~aOJ]{}6] Lsh ?G(fL &9u; endstream endobj 341 0 obj << /Type /ObjStm /N 100 /First 870 /Length 1751 /Filter /FlateDecode >> stream xZKo7W@ -$95rp H7kz- '=f| CBր`-9@d! ZPZA[o)Hނ6EHg$9T{,Ű){G1`t0r)N A!ԠT)\6LCZ|y`dU-32LoF}}3@ }j맚.xm w+BY8ϨBX]BƊw_TX5ybs6mEMg^˜| :3Ub1XA3ٰFj%r hR񌟨Ќ0l>% V\XT<04苾! Zu%AnXdƠ:u#2V.@^#AU S\q$X`w@_O̒[`Ũ sX 0)f:խ)R u:@ۙ!'[j:(1В۴duvr2gw) _~ K: 2O>=yr֪-b]brx NWpr0X+h_$~Jc7p6B //W,_8 7_%qXϡby}GS|6Z}X\uA{gØ4k^o1%k #uUh :߯/͟.-.r]`Q R,4Zp *E8 v6~߯ެ __-Çߺ&Q$GʱGr gXH%?ME@݋@AlL<@Rt 6L"y0ZmV,S: XZM[KmJ[\^n^yzq.3E=#zd]Cjc M],bl0|Ps4dO $e{m_ZAZ ޸+LI-rhHǴ^.Se-66cj yf{> stream x]o8BC mm^Nwva$ږkEQ,2NlJ~d{b[D3`__1)$յCT0Wb:އzvX~p2jO >f܍Gz 0H"z۽{| r${^0b*Q_wH1IH%F)hwd |p=(LB:Gɠ,vn;v$"X;guR}Ў@|7,IcwP}J#`jR&ea4R"`DCb4{ߚI˂6$o =@p8X뵈.vwC(lvVR\h J/FRs(U;𸁧}*c!,?榥 B$U @{ZPN 4;Q8WFRXes?T?٩qs4>8 1R׻m:;n؉&$< v'qco#ge K[,I/j6(Bs2f=u,mkp?z5;OQ+~#߂7l> l&0G8+.Զv4+N?j)p4uKGtd]EB! 3-d ۖ pyYFFi.'q܍'q"ƶN2ιt>VHh{(\0nԷ imrj2b>A9s#a X̅**oW'a!oyzfhɨ]Ґ<˳b'cLw$G8YR!/.OT_43: 7>ʨY3ti{=9I]dϼTNw8ta<N~JY4lqR#ζ∜WNbՋaXh>ۈpFLFz{#K4JZӫZ;w OV^GotVZLu8JڀyAgğ9(C(0!vլ <|:ocs2Wc:uF8*H$=g?tg@J֣a$^oRR?xunQ S:6V8HSĥk';҉60tA)?+R:{:SyNKU:摇E߆޴TtU +sLH[]BDldO}Ԭq.2ӯN`q*b"?nN`Wah ( w>]P?dLhjwHpU?b{Y!@87ޜ>ȓLJ)<p͛aTF[wB"$BJIiW/// ud1 : 9('V$$ļH,e Y]"c`ES٨RWp9u }$Hby4u 쬛|$ u2h'$V0]gռlϕudO.pV'4*er,F'V#Pᡓy>nIy8++c0b&<)]VU=LKDfFvBtdž=WZVSd3Mv3YޙmO漦w|TD]k^¤yȦ\fY rDx[71#}+:E2 YQx=@C\x,x4"$sE~D h>KmJvh~V?&߃𐉃rٷ]_pDS&9@5URx||,'@=6.g^ endstream endobj 464 0 obj << /Length 3145 /Filter /FlateDecode >> stream x][oF~~z2`۸du\tI!P"HUAД-ɒ,|\Μ F`ǣ.@@\^9qGԧe)ދl4i?彰H[]~0 A/:ra츺Wx9߱A+#΍5qw\wGaC%ŎZFPO`]Os<"Iqdqܻe}5}{l,(E.,p߳βG?y>.u\ˁd6úZv4qa@|_q1„c<0O\D%jwà1]Qi/=W/;NJ!s]rixHYs07@x9U-%鞊a%hAu @LTCʆ` @y@ݮ 5(#Dy gqڴCn.ݫtV`"1Z%^mHE9bN-M^9EDžZWrրu +Opg1y{bHs:>\H!E #Hi>l8/ܘB*-ju˒jF|8IhYnjY Z,ebo.w&ik?~]洃G6\%vfts;MEq kOSkZ^NeIZt>+a 2oENH&d9'ϱL>.w+4?+|)s$]![t>ER,rі@x(y^ /KtU9RwH6kiY+$}e`vPGG4r1Cb ӐdY^ީV8,Y2O7Iq|}V""XCX2ߴdNjzghVrז/?ɱu+bNxM[ M@l/4@}فerr|w/RMXK\`/Zj]+ 1n{)Ɂ)og__|S$|qNdY g{ NqP`?x{1.ݹe_~&`@ޮ8$/{g}cb~Cg/^\{w1Y;iACD_ > \q6iNl{0b=H HrvѮCl+;WAlwĎKȁ9)L헓<6R̔x<E\TeWU@h1JA%)7G"{ jDXkS|O,`[i*y]~2_eHIjj"r:/LzVӴ:F[vpW[Nc[JA*隁_<<hF$)&52 dd?Qra.fz<'ϳ?a~  q:|c>$i&ײf*sb"ty`vEdϨ4sbǷq7U=r }jj({ i wu]N$yQƃ&6URi9?KvWؖ 3VGTǎU<&0mI[>-h95ǩNqOߨH]^<`y:%0MLO}* 7RExCgzUg!&8x͓K+EL8h?-$G}ޥ%NO`^Jdz=.Pxdu&DW\kvB<zu2ӏ㉮QI<*.ɱ,~fW>uv5)IRIT4?iֈT䤥g'U(o1%Wm2V[CP=Ԓ9{ˡS/+l5ytj!`u<1TF}joN1'WR-gqX"#],fiP`.gR(ss =.cxu]IP6pPQ#@k<߅.PyOQdt|LV_X.'̤qT&;7aMqT) A!K)=h!|`AhkP$Sa5+k(Ws) *KǙÍ6{ݤL e.B]m\>2^^eǡu l롩b̏,Ҹ]LU]&LG2K nljt٤y#P,j,08p+\f35\V\ǩE3TQ.](>1V)^b5fq1UM 5X3#<=dNZra<# 8Y0~t29\(\Q-T85xKŴ(Ɇin$(O 6 Z“^ b!v.YH1e-L)xh6_XTyY,0'.Ul t`}…jt _ _It*aɬE+_mp*7և&aU0]~ >`єhd+Lvh`O;9OB`x#Vn%gn`4F,175E"4~]qRĖVWJ69 :>2Y'ñiUc.s+nd8CD_2t@`_R_>0oRߣ :ޙbI~Hy*x 1fߕV;uX endstream endobj 469 0 obj << /Length 3147 /Filter /FlateDecode >> stream x\[s6~z D0$;i:٦N4-1[RHNf}@Ȓ'MLB\F"}ws!*JD ѯvt&YS'E̳:N4< hĢè4櫓_(iݘ^H*Fx}Hq%h^E5ϝ X2&,V8:)-6(W$ Fb'% K%)kaDH{"Sg1JioH~ݜjc²n<7jx8~@{RN+PR TF`µxV`Y9Q&Z7Kz@J/d}(H~(gRh[Liх%$q+Ikٳg+(?GELq5}{-%UJd js(d ˦(nf?)%2/ն'(7͸4r4_txrS,͓hJoNʖp)SUVaP7SA'|n_RE}zYw!X llR\㢨yb9M]QC8d2J 0 #.c%a W +l7>5jl_Xl*-lU'R/om}nf7ؖU#Ƽ`KjBq:! x g_rF_,'D澀iD37DVZm/-EM]|^Qxrq+jqC:r5.Glhz%z6.7*\&@,e>0ˑ{7*J;dbԶGf۲x[`rrkci'ojkLq +ZR ޼#$rbv2OmJ\9O4 EgmC4Z t $~@bEY_N__Pʱqa7?QN溰+bϪ|D"H)x:33|a3l(aX1 cV3m0 $a@g@MZnGh- |kp h`S73C 4ژ Ԯ@ m˕/z>y堽0="X٨-# M +7eFbP˹nI㞺}c& ll۬W`veQQ?MM'|V@ :z(qc_.}Wy䋿9sm]k߰p(lOfudWel~[z5P/;5 uvQ·fvp SDLX*u-mL+LLѕ.󕒵AR6s* 3|vn; 3l +06Jl+[aIٔ$uf;nҗNDR5I RLDhԄ5x1%W! zKhI{5}_a໙w1wZmz{c/GO'4,Z5<-wu8 JCCt|Zyj0., c|=M?gدG[ q_}Oߋb?; sF۠ -ޏ³˦Q>+ŕTZN*"ld]eLCqaӘ &l&7bhJHfdg+#%ryҘ$0|0(&2C31, 4s[149n0 iHHZ|/ 1|LqmVn%POڔ nCm,umUB>Nb$$0vY[!ŦL&aj-+y290+4jl<৺u1#>=e&!m"}֙r݊yGL;*|^)D7PQ•N4Bx.Ҫᦗ>ߥ͸ ŔCV9FQboO^;1g-< x$.ɑ&@Zqxe;R\țr/!2<#!]ϐ%j5d :{wuwi9]a Daປ N:\?^O'S౅hCLd!;z=p<U]˯N9?ur9_/d|uYr]cޑ9sCVtr$ Kgft~@gw);;$C8bq݃Csy& i` ਩vԼ9a|`C‡ 5&k{VOcvJc,D,c9|+˼:=͑DU P(ܪ[p =O85a;4<~{#@Yzs}-AbwP93 xM`^SًumrP7jOb=Ga߲XYrG$*q ̅i#rW9v-{U >P8yI%Ǯ$. aU81`H=utI"z} ʰS)8;QAޱ-)FdX!6$t1cDԎ<7/? "%jIbA2JvF2gmC?h.ӭ[Su5fn!<醨DŽ? c1z;~ aG\a$0~l%-);hԺJ0mk[ߙN'`^ΘZzbRh0ў; aΩcR 90;\H48]7&i7A7'…]EQ |oǎ^|/!5kqLgZ\H:q@5P?CJ endstream endobj 476 0 obj << /Length 2591 /Filter /FlateDecode >> stream xiocDIN&mЇ@40hpEIPكRJ\ r{αkz{{ƽJb.Gއ޻_?~|Ƴ?(K}<46$N<,#fTD{ɇA;#'jqA%O|-؁{> }LR{H߀x8el,{7Լ->dLmMtjbF4ŴPC\z}zVc-$ dHdDaAPcӬVN=f!/ "@rJ\ A3Z`ްRqm7zᱦ~ԡPTW wpg ?HHRkS_`[҂-m$ف &If 9 #wD`wU]E>T蹝ʔa@IC\N =#Z`1(4#/+t>>ϻJ@\e; 2$/+QZ[6y_`ş3y[,9Y: 'yſUEt>}a`%t3|2{4OeM 3 vf`\נ.4kN@>9这-FhʩY=0I§q22 df ([67keA/ 'ˑ6v V R986IV%9~O 4,hu7_72.v" MvhfG}{}4څyKazd2d9̖ 5ڜ|MѷB>署I2j zZhpŨl DRмQݦfIG$w8%].@_5LʁlWآ*P=RzI-Z8^G Íh`W0$(sWerUƱ)\vb 0 c2 |\rytG6潂j5y[K :Rpy-`U)bF=>Gk*BatQa,|v§!??=@Y+`+Tl9(ؖe$•AqeaWNœ 1%-rЯ@b B,uh*؞ ےb\qKGISHn>*bsFScGx.Fi2<%x6@< 8Ut_3a܋ߗq-yH;Ĭ ӵ0/i.0PmUemCBSkӝ>*֤/(TSjIui Xa3g2*fT<!FGF^urߠ9M&]ka ݯHP.:Y~6=NfIv)i_sVTN1j(*|e5cգyhqhyD";޸: l\ nI.eY٥{/t"v -RY|ץ2{ n}0$-.Y҉HûPVѡMa72[ŭaʲfȯeDUxe9IMdټ$D@:7' *.OĂ"*;F2.&t^Gk@u𠠢NtPs4Joͫ/F5 Ȱ1iQZ>|}r4OmrwESydG'It=q9ѤU`6Kܨ9nZpri-:r)4$Q%I{zM+w=m%v-*r<-"l(dJ5_vI=.|.8g`g]zv J$[ě1#8a<ռ6Ixu1|akxlvaS:Cu%YI7\O xyn:8;r~v20`מa}Dr1USfOi(΍H;gCBK/E#5"x{Fn3*ٛn5֧twߣή.LJuz*tSƝvysiQO~ B{S',X=1TT:&MNCpoBZS(؄mH%J asݘ%#jf pV+Bi"qv{?\Kub+ծ%[7K>| f\}GG_mMZپصnNJzAe$O?v!Tg2DPHMohs8Nۥ5autQ-K#uAZnV NBp ~4 5j,7ㄽ^F _3PD]NIRi~ endstream endobj 481 0 obj << /Length 4073 /Filter /FlateDecode >> stream x\sB3 orI$LiRgK憖hD:z.vmK>zM$. DN9&t69-rar39_L^Nዯf?ݪjgtZn꿯UE&Oߞ|u~ˉDMQv2_INPD S7jJhrNdTPԖ_tZ5T-(\T:UE7+^.=qg5v#a5t7U7Pi~ifb!FdZr⹿ N4^ 4S,hy{wY2oumGkf}ʺoW1S"O E(hoQkDywnw%)ɱٴ\,^`Kj` E =mE+l{[vTXX*%U1Ak*|[T3'g8hֆ$Ti%l %̚%E a5T4$޻E^]7*g)R2 I޳!N/edN 0V<ƪ^ w,vd`=IR]=;B>YGfS3Dc?W͋ZK"-@; 5-0JӰ}[ z2f3V49zG*r͵M\ł*Т`ŝ;nZbN%d34юC Y-ଘb:ꂿ`䆚E~_@ܗEev8$:-$1ҙPicܧ҉%^Luup?%ԝ-#4F` \k_-$ۙiW*cyۺZ[ٹ-ޔaEܞ1b0+; eL$M0OcBZ[6B|R 2 B ,= -Qw恚pU\ϩ3q0Sn_mh0Ux%w+);V ('ճ]mJQougaphy譇B`͢r* E5vEgCɺZYȅgi;3!77S0yS *#C.#vM(2~mJkN -1}.'c8ٌ=A{SPr۟P>>;btٌ 8 zl3犽y |1jȧ/Z. FBw`&fwͶ|K=1jdQހnDyDi8 Z?A.{]t[Z:I!tkۄ',u?uu]A6 3GyS0Fi5xීtÛR-pve"b?` P82Zھ(+W˧/OA NzFW\yٞczx6%֢)ws 9Ud" xHDva7 e~*]"Xiu9HcMOkk1qb,tgb\V9N8\EKz`(06?V_T *?SU܌\iRCY&X''hz䶦SRF.@@`l礓_&з@ I$u竉L;Wb~ 2PQ| P6.^cfΌ@6{_܉]r9Q ; t[9FhY ǂy摽07ӱ1^O 3]rثs ☓{O`? [YLAKb'~9>? #y5~o*1jPlt(u:ThP`#_ ̰[=:k(1zq|gȯ ٨thdcD~2c YM r(X%$Nsȇw %$"<7 N{:c[odײ&Oj-_=W/_~WGR 2jx]fo:?Sh.`'nA%qME8|.9ų;ؙYT=n3}離~+͠b|ש9SCVۛvgCyQ 8WC Q{O:{X~>V~7xvby&MΎ3oi&2"_+$<'O+v{77VP(_/ X.PXZ endstream endobj 486 0 obj << /Length 2568 /Filter /FlateDecode >> stream x\6)p6.!RФrhn Vv}-7|I"Cjsmd-93p3&#<]$i$EO~?Q ^|QG|bĴgy E/ξ<(8"TFٛ_p4WF,Mysp=ynR)X@%[*T¨ n{3H|\sw,mOf/nesz i#"QW>l@`P> p6u;.~m+ś&&#NhGꢬH9aH$½`D`] ,U0-pq[.gO * $<6AhT$_;p#y@!ң-wQeÒ!d< ^59Rh3Pr*/h6)hvfHF`z j{3ϖ;%ڈ֘zf;%xn{]9 6+o|\/VI9R'УȑGe00Y\ͧWDXV@L:9+luBo&t_5ށ 6dYJ:`+wh/k@a0X<ӺOo]5W@Fˏw-1G }L_ F\o٬Z^FX+&I S ) n ޕ )/mSlb>Up)ǀxy[m>OWz7"TgyWɷ0|#T\]milɽ*љԏU9u۩^v*O@`!@ä8=0 C>)!,_ȰV|Uy]3G )JXe&._+kJ7!RMy A4:#:}B-LeA{ʡҤ_Ucg_s@)/TVUƣ3!֊?wڊBМ( l)`lPśU.ꂀ 7:ōa+ 9ݫ$$$YhX1{]7V:\hU}~%!J%!+ʃJ-E E o] ^x j`iÄ6;ʹpJ*$](xl~*JUi69A8t&%auP]Kw%DӖ ֢w\CϒΝy߭VO(%A>H'k}X=V¶,@Fs{ܨL"p$RnlO@zϺ:lkTteUHpRy#ƩQ9ﲳvO&RRjW4*jۦԮ&=DQ\4-[lf/vO3m5J`W4+z]:ToC7o{qSGodw+^(;bKE?qi ^{w_[ߪTlYOUQg GOKiXߕ'oIwNz+B*T?nIMoCHo#)4XȃR'̳RIݡA[D_Uܤ O;?`-,Ib (sh@Q2_Įp0E> 3u{]lW)Z"wȯΤ9nMh;P(:Ičٻu:G~ ē[x" di=.Fϙ;&b黛vL}L !t7-Zm.J&vF endstream endobj 491 0 obj << /Length 3795 /Filter /FlateDecode >> stream xk۶ >rIĽLډ3Żc# IٹN~|w$m:SÉ\žwՂ/|t~gB.2X//B +HT,׋/ӳUOb-ly_5Unʼ+%fgߟ<䧓PEHbXU2%b{|g"KoͨBʼno'_pK*$)"]$ @qB#n(p`x:f/8H"VBEr5A寯ٕm7-Azmqdd8,9g_~B7-EUWY +l]5WdW $&˿GJcBJ#G,=X i*X X?i`G2bn?(Y¸E".F" zyhRt,df0ѡU@˄]mZY̗y q]v]ۇM׸Ms{&\c`"Oϯː$,و*ʏ4.=qFi4BVuUF7!2/< IIM"1V0ƯEکW[z2;JO8m>wU9ib \ }@U" [r0t#pq]?MmTd}A.fc*GMw fŁ uJ$1Kl6w2ڤ%:2ϐ, r'#r/o;p*;i44{wZrog M`EJlN Cꦶ4ƥ--:`-CH2{R>sմ:Wh&L$5.n+W{{tr\rbꚌ߷%^mnјAo]uMN+V:F {píL6zӸs[9.6_ly?4d\su9T忔uP\ xac0,Qޝr*ޡ3^3-}M| }e .,{@,t-)TjIwKEJ6; $;Ç˙(B "}@ܜ/T:dPLּyC繿Z/X’ WâXgOc)ut$x*Wic3,9 ۅKi`J2K=2%bBB;bI|L,d%A4nA_6a=>+Eh7{+vxz )sԥǪ5~{21J e߉Y\7tL3qe$NBF<Ɔz9Hj`JgG'S埌l?*Wӻ1fH'H@jl=Jc@Z# d"^'Wvl3w猡oO %.KԾflN j!5 5dK?GO#!oyw_BOU}Zd۟Ni7Y@(L|StnI,Ԯ$,N }4˯6y{nxʖrWӬHPۼчq!ܕ=xa}/-Yez;C롨tnxL@9@ʺ•.6X xs`ݸd}>7Ml8a0DCHE1lCS8 CpsvayZl֍Ps`qV"q_ZPFAYMo/@) -I(qn!y}E9. rbvy}qS6*vtjλU 5/s -߷E޹~QS Y2)s讛\6q6wa`W B-l(;.g`:ZnmiZmO5{OȔ 3]2ԅS{Ak [rf:K72t%SZr\S;DJwm#g׿/o>􇯞PY\2!qso^)gU#72Ƚ2:h'BM ] =FFaܨ*lU`uE vI1ttG*U)|<ɐz1M-jI zxygm :A9>%ϳpbm.:Ȟ@)Ou&lfǾP3:״SȳkȍU``uB9K1GM&[kQ1jjx_^+l=/ 7;^1,.mR#NNz%dvַo;1eC Mը~};:0lpor Js׾8 Z tKttN+"G35DKLY#C#v- ٩kub3-AÃ)%!SLaL.MV"NAL qar[BX4b5x<9Uz ?KO#AcљNiO;7/Sl9+, a  0v7/3J޳OU) =3c?RmmN bd.WyWӣ3םH TlXQǓ;=~u5%> stream xko~f$ LJz-.}(r%Q6[tD*iP򩇭e5Q>>ff罫P֡ۋ7/~ Hvn/s3q>~~f05/fQZ 8K3Qydh޼xss*afVIT9Ň_3;̑ixO^tA--u<x!s\:D1Ϡ UtE. ;g3v?\l[HxzY¸ou޿E+O4@G@-kgӞ'K-!o3n(0^ʿv}rs-;u~rՁBm?#S ^`+i & UZ.rRIYEP2xxI HQ ]jm7RKer_W(rt·󿝷]jB L*;fK@V &> nt +~+Ak&b/9 @/{cy6z-#TrrKKOKKKYqͿRuO͓>BMe 0q̦H;}l5)nc@~ w>C dj9Ub)Ur)AՍ:!6-|?Y Od]^n5?*|N!|RCܽ^A :@K0t28ű.\wչWOhTZv^%[ =#0RVTmB; ^.s4N&w6]!ceeI_(Xy7Ycp%`K-Kw(*hn  A'%_W<†veM7,ejQ8!+nb2ϫUKG?~jͩ$.em_ _Rbh|s(b1r5ċt pdc30/{$ [SVhl+MݴOxa[Fal~ZklVQm؂A2,c#g&<ͳ<`2Y@mf~sż*ĴDMT=+1 J] ڐ1&Yq,|A UVe3Fa֭FskmMJUfOٖWUWJFÕ܏gēZn5o+wt]0E ۓ8٩#wrA-;~u|1SxKY 95aM!rG[GfjpljA15E[xv?ᑿ}'% [G&+ c$Rp&c4~1=Xꗁ '2D 8L:? endstream endobj 500 0 obj << /Length 2881 /Filter /FlateDecode >> stream x]sݿA9&y8ɝ35)$:u>O$ʖL+| h'g? $\ׁPp%0Dp1>ሇygb8R׶}6˒Ufh‡];y{q hTITo4CG5bg_NCRµ75 `}MDH͑:DAD&GIDdc˃&[VLZysRqxDb-JyVLj8 m9@Dc5b&E|pCAYp#'OEȖY ƩA|CNO8E9y~mO3i5djPd7n> ,1-`:d@lz ޕ4wfm$:1aVxc#Md{5 pRX8MWr|tx!_ p̲W녃5]cJ(֐(N%aRn:sd"FD*]PCt\YMb;0Q0VµӓY AE9U7%}ulNiqtD4䊀 ~`hJjG<i0% XĥƖT{Ahq)HßP*v7?7Knݚ]6M%Z.ИQz< m0^~ĺ-" `dDsEk[ 횒hpb4 ٕ}TkKkʑ Ioi00"zH9z)o_1l z"޶ġOt>s"6 mUeE /[$n֍Ŝ0/:AXhs.t_eˣ'1Qw1LOYM; >dlNڙb2wk+7Oz/O,?N?u~.ψKDwUj9L;' >ѳ$U|Vۜzw-׽3mA"G?|Fv̵ܟ9yQ _{yK4hQb4Ln?Fr8F-#!(B! endstream endobj 505 0 obj << /Length 2349 /Filter /FlateDecode >> stream x[ms6_AV0|IISLn&NfҎhTIʱgoER$w],v [W~<,uK˥{llaztF]oC2߮8$ȣ$F WaAWGgG-2}ך0}(G- +ѯGXĈP|^Ywǒ!€[Z0"35ekHr$(=(sDq_BlZ밭yE9G@" 5Ȋm7UkUp(CˆpRHKW °We%Pz$!\6OZm4Zݺ JB uB4v H-7Bb*eP&-+^ =Xg@<ΙrH.A<ә|pFOe0V6Ri3BLw/){'g #w"e]Ռ;;:o!(|'By_FWTFt^PYS@B_No&L#`W2I׃y?γ<@t:4uYׁqQ^C sw*Z$VReGٲ1a R2܅D l6Fxqh{H4R<$Vt_^WwY+akFa5rbQ<rln%[ V@'+ZoT'fE2GJu)L|N4Š4"!+Dr-5sj>Zӥ$)HL7S 7Xӊ,Z&L R7hvƕB-~Rh˅)26 ݁լ8dzbS^Q{ӳf /(jؘ6vZEOl۫Vyp(=X泗3,z(߀ 1"9o|P'7g#Tmb8wG~WV441AX!wZbDro_;)9ti1;ܴOtP#8S]qxgR8(&Tq%X@2q@$ ^I󀿉[2Ӹn(7O<䓧#ҪR*&flnׯ{Ojα Gة"v_tV:Q_o (ͳLiNe2֘<)i,IZC}v8ضdfBڶºf}k^Z(y!r3";:c:/0Cé,M/x~[2E/pO~}ɏz'Y8O$e:{*"Y%#5L?y`3efllU~yGa%k0#'eeԘ5vo Q֤||. !GW ͵M]G63PI="' 237o~.PVIP}3m^h˖vg`.fy$d]) =ih)Wy>`"xƟ!߮Xō]>H_"]n _2ƅ/ !H_'2 ۓl:2L$;7t0\A<7tAĝ(X5a^ endstream endobj 509 0 obj << /Length 2890 /Filter /FlateDecode >> stream x[moF_SA <8)6ȡwHhT(*q~/|Y%VٙٙgfwǾwN Q(N/<)"z2  w:>߼u<8-Cޏ$KxGt >98>=|a*Þ$@!Jo4;0 J5LjH߲#L8 3njn $Aj~/JnẒ%Ab<@0z(1@QGBd4z" .Q*2oc#[ 7A~}ZџH W)ʰԣcpkB%D0Jkof;UqPEPG/Z KMUcUnzϕ> ڀRP3ڀ|3Or0`5r'j>XcÌ3<<{-d0C)dhJ37&oN*A1zRg!a $R@%ߐrD!`iEFlỴbNF*Ƚe<,_]\4a$eR$L+&Q:&:ͧmjU00"L[ fBx]݈[u,H.m9.ì<͊8_kIw,SK)äz9w3jhY58 HˣZu`ߝ͑#Vxhx9$:/?f}BBQP|=Јua|? u"rD#o/߾_Yu9/,@@0$-Ζ/PH+g]|KZIU$ x v0A[G6"Mm#xf?r%[֓pa\ =_$8+f% lkCq-j0_lwm [)Kug_)wPfuݏoߞt|0"Ir5fLKϵ%l{WG|-M2#Gҵ鸷C1RֆWz"NÖe $Hо㨈8þf`U]yzRX+2+ nxm ׶rW #g*r¡#|:HAĠ~uNo$bnt%su#[d(0y|I墎P2"uO`fN4?A#!B,QIb*_1 +8{QYD#ET":UMuy ` ڒWCPJ<"nIT*sc4tY\ٻ zK pU6 ^Gg#{ТӀe& ,+ÁAzJ,Tj }.Z\r88{M0(HlSe:vҨU ijgP+0i t,w0d#t= lr؅.%JǧTFƥ]eAbUupbPE nqSJf6W[Uo_#b4ʢ9:]Ѻ.Nu0(ZS4[b;_f+ ,fsecHqޟ^l^A$?󸕺M>|u ͑.E[uqSEYQGYZ$aaܸC`mC)wfu< endstream endobj 513 0 obj << /Length 2503 /Filter /FlateDecode >> stream xkoFX}/$)^ _\Ð%UTYrؕ,Q,+.wfvv^;ػWH*OQO>b>??b?h%px&7IDO~9?tB`@y%È%~jqAUOz ؂{   - 瞒 h2_cKӛb|[>'ilXD^Wc?Ab-}$ &6ch1C T qʥ"08 w`ġ3.|ch.8rto_= w 4CFÄCgu 4 ?iϐ0 O:ܒZ@j@u9e{ 0Ú비 0/RTnS 8EϾz/UA<0|r(\݇;k8 ➾7 &X')&3L ""z< WP7fp(`V|4 `> d'[Qrf|fIDP)2J*Juhޣ4X9Az|mPn'N|.}81_PRƩy26oӁn7KqaQW'^Ӏ?oȝrt>{`o u,1=cK&uc3A/G6{$PT[q0Hpv'v˶mIl:Z]jIjog : GY[a y e6H Co |>Ge?ئˋڄ1w>&ۙG7)2P*2ҐJ5uό%ctaj#cJD}1r.z1lS?O|_O<d@ȴ³iK_Wfb{Oͨ=$ ;Cl ~{oD t)W}6kV0=N2Sނof{"ӟ-HlFP+4 mDۉʮ@%q]rY"&)/< H C[pL8}Z/丯 br:ϫKb|Pw˯ѝc6SP0"{]32/zwt,Vq"٨1 3vwӉ~bE:J/pRpDr{Vf {87zr M4iYogswkzO]w{gWibL\j.YhW:>d.X =+Z ) c쳂}VlAڕ?Tl5gT+{VҪnM_ćU; SfX=3߼eoi{\d ZrٽW}r^G)#̴ҩniᦠo:!8ƱW~h9=l͖\eC֝5'ZQ{U:cOnc#|3Vu=ާ7>u+O_L770.L(M0SIZ tI" 9H4A?G Nz>N1j)K\~^ xCo{ {&z>Z'۷Omn7.jn }ܣDGᎏNT }ZOm@G];?Q|*%>Vۤ endstream endobj 517 0 obj << /Length 3350 /Filter /FlateDecode >> stream x]oݿ0 '~ $m$m8CH"RE{gvd pvwfvvvɂˀߜ|uvk$L0E Aġep ~}W/DVf-B:/W]۴A&ξ;'b"P'QmN~+P&qp6<:F,$bIVB* 5<ɿ2ͲboW@f]΅ %B,tvڐ ` n{7wõCAL:6yG J\16z6$a02Q;q(J(OJ'RI =i*˺t07a@ԉF7vӠQBLD*r<޲@Ϟz %=iavaqEuIb7  h+Rx}ֆs 9Y[-T kMfҍh< q>  {dD`œQ oG?$t<]٭-V玍sy`aC-Vq! T^.Kԡ4J,afiwTEt@] l#z7Փ׭ܣ2 d`^-=4Ga^ ΄4/] $+㒼=ʤZ[fFQ|mÛtbr<I |P0Joz71ق0j͌-un쥢ppF ɴӍm/<}ꄂxBIœw/1-5FzcC$@ج<䏭rb cK%G]28^[z|:8 X|:䢢Er;B&ZE~<h9cm ]TJձȼ4ƼS](Ƥ4.V!KWxcSv^ӮUQf?,OfYd?YwŎzAKef6r[͹ksf7.XȰ{W}lmZxhR-=g3$eM% !e};YACYeut_1@*7+7˼2B^F]ݜv}!OEөЗ QkO@+#{XZIwρw$)!)G Wh"F,GgܧgE}b#^$~^x\DaE ]e ,'Lȵ`2':.P^~8uZ2c>ȳ**G+/< nk׮tlVW0h~o[RO}Cu+E$Ew6!!FQ+ǴIھ"y'#q-mݱbkh}U1^N1x.ӕӯV`b+3[`qi/ dNն,V] ԩG"r̟4:U^ڮ©K{)NYV>P}~+o6hMn%FGYEz EUI`;&hU:EkyWJ% WS6^*VIXZu*O͞ӒPEǜï ~S Mgg K&RA\$K)ȏ}!c$wX9﷫=WhAvS"C!Ho^PwwZ]NNҿ9o* k=|cր4-ԇvzy[DK 5+[#/q]9׎' n)+WnzW:uJy %ŋ<>(# ,mٴn <aBy+5)f1Sǫ*hM, R]ʏU6PȠ[5e( $}{ᇿq|g{;.P&K!7;+]At)M+p rjqC\{6Ctn ({E=~9vG=n#a$D¿x;npAܚ;-tmZ]> 5\(h=A,Dd͔wrnp>&SBpTQ^MgmwSEBfFU#j՛dNAa% uk FT94đx*GM)j?8Y5{УjP~q{<(UeРny-j[w #I82ϵIa@"& iSsɦszީy&lmZ{L{Gx8>t#f8nPuYnѽ؛^|r:lJi; M4Qg83 ' Y{=uzSM: YVFگ<*zJ+1b-CRƮMt]#ܷ~[ƧlaN++4Zh c0Aegy_@''ӟg S{)yۋɽ&gݫqj1YTTNmg+(L4}mdYm !o W[Z.$*nJU Qn.LrSVC[Pfĕ(Z˹ [OXJ :P]IG *&8%ՐF=z<Sߠ)uCCL]"59-/@W5` o3 Zu9BEe3bTJ0s67]5|H MuŸE }p)ڛ`!P"_Ԅ׻ـEFݎ\gl ueNųG/D/5wE9P+އI`NwTVw8&U;Hdahl{GiH1~t5%NզėṋH|;CU{CvAJ[ endstream endobj 521 0 obj << /Length 1985 /Filter /FlateDecode >> stream x\ݓ6p/}ӷLڙ$M2NSahL8Hʖ%|c oWJZ{7^w_u._1(TzWמ ||]7?SwFi8[;\N]xä@DMUsSGr$卦77F,YS 󶃍ؒ{  --$瞒 ?`Gl91%pzM_EأeMFׄp%<{w5$c XV6iNq4QDJ*ߏ8R?1hy\S8\sXm.4?rt_{o=Pw uä  IUO3`0<UZKR} LCL 9m>4'hFhVz]@+~U{&FT@*29תs/_Ic&Z#fד"慤&BܪO\$7T0` Hεj a b8 >\:_Z×d9U5䉖!Je* dy[…Qt\XZ%"Y*vZ?pM SpMZM{[p1_w{4Ws~[6"j>.AuI YQAs&8MaAiOp[P~ 04'ol4O|0[ SFL}zvl>{Y݇->,= ejO0&p݀L>15<\>SyἥH幇xvf?Nx{4&G[xY!v\it6hdF^:K\a~#BG]#]LY"%"ʢ5@˔֓ps$wx %A:GPxJCrIYg'ҔXfw^~i%|SZEJ@2I :_  J \qa7mt}p(1+ޕC`kuf׭R*2i=0r>\nC$4&b5+L?qՃpMU;v0gB&txds:)ak䑽d{_hō<=1H [r /{o1TFSR߼3(M +ݤïy?_Aj.RsӚ3M>>h t @٧n\6vw[Mclň(;ӖU&,7l|ӫƒv< L{\~į]c?m ً]iˮuӢͭuFz*pe" j=ucunznդ6s\s{ΐ\7BQ̑ĪhV&VyZyޕuEգ潛@vVA{QTОrR --ؽq3! c?) I!;C=gi I:y:ZA'l}lkz>o]ob3Gr#*KRw"Ú4^Kv8A@ݎ^_L/ld]=%>/Pؙ endstream endobj 526 0 obj << /Length 2583 /Filter /FlateDecode >> stream x[[s۶~`=yf"wsLӧ&S0mG"Uw)mV8  >B4e \D!gћz5pOEEg]d58ф<sSрUITd4A'"n4σw'P*mLC"C,9e`4%TiiV&3`K*_*s5Wؓ0f9%+*(w%f"P%uouE .41ڄՆT!\jlI')xCP iN/N9}/vPxy]@0ͥR· PWTPĝPWČ4V D! PQ"YK 0U[=I*kgΖdorB;^c{C(3;YLVnJN"BthES Wn@ 1pD*n-NMcJ1I vz:F>Ђ0$Tg2f)BA+ф8&ߤ&_ĝgGweVwo<]㎠߈?z+#&ht!&^d<њ `&BI6OKw-1*/gq,Y{ZXw7~yjsuaL5yY~ R/EEGQuhN$ ;mQG-DzMS_Y 5uJx":taf3M)r\NxKmį㱠8<-|KIIk[B/5ZevBL!5G SrH&۔ѻb \*`jRu58pzLx>ǚXfU3\&l`/e\41~번We(Tnٲr>ՌaDF2-?]"]=ESu Yl.A1H x,q܍<$y\:o0;r_L\ SbS[|3Cv.f DrUOyr3/'d[|ނךd8%EU%]( ]KW*YC|%lKwGhvETHΩ\.s;fTrKTf}X$fD[/qa6K=+', uKi0vcK!㢴#W*S;$s]K]iC`Sxy**?$n0p 1 vV3mq]w[=o+!U& O7_eqoxX`3˜Wvqs|pۣ Ц~9qKj@ V@~SE u:WN_:[՗+nQЧ+uճ;+d2*T&%Z2@O!@%1=d;!-N(nNE`Z' y|1]QZe94f:oVKׇ>9.z[>7޶TJڢ'\ ūqX$<i}p-݌M>xX/a3\]|Z^t;T n,Ü Yw 3޼ ;:f}[ȧ V%hɩX9~fp`R;2673E|vVjYRq$ ]>3WfG=]~|@ wB۪i_GϺ_bkRTPXᾘk{jk{h?p[t]'Ϳu?` YbY> stream x[o۶=Bk jwvm[ j+w|- ?)JiŶdΚ<i.ptWo4Ғ:R4R:F,f0y?u{Tŝg$fݞ$h6}d $?_tuYpDHh &G1GS p?~;(- - 瑒 rfl4]CKb6拴KElt=ɍIa"BQx]D;w$,1S3yWjClǂ`1G TqlE8ҴgTZTaa01 S7usP\ޕ4%| J;4+EHvkFZ Y+I0{t ba#\od.‹U~ٽʤajC\z=[*恃(# voi2զ T8|Ic!` BXeο 021?xq)5bJ_˛;sTR f bKvݨB2XXXF k=9~,`k m|e"kj:k ΜR E i2IiŃ}0bdcc)](=@g#Dy/Þw3`HqXYwjd- PvU5JD-TC |JwCӖ}ɴOg}n2D?%^Tgr|tA%ۖ+n"Ͳ#++$-q ق]0?lL[(qoWcNP2c jÿCc "SӼ?Gef]'XHN wRن\ W 9K6~  Mn9r}=f& K$b"'89{i=к!>6DjT!ڏtp;Iaa>yykqH;R3z6iHuueѴKDg<$ 3<_Z%K&cxu:ܥơyBf̑erbY1TV= a!J͑t*ɼm*ݸ(M&X0Ə D΢߄ 8GCu42*z7"qH3nA2WSUH;o% ֔pP7._sUS+oIv E2YgZhta(: c`6Uv`N M^/wIf9KFZO=}Ra6ݬ}S>ЃB!dJDZ[")Wq%*% rP.6ҭ; @ӓ֐v6cPs+e `詄uy٢g`.T|cAr,C C(;/k,_ŀ |"{"]d@2,OfíGNYE/*)|X[*пm27ɴ@V!,T!!uY=(iۙZ}v lRjt,FafaíW[k:BhlP"w$WDY+q_2Mok 7gpSފ֚SvU]eN6sL)rG=뽸]Zt|Ox gvD9؃ER">^HI)#ܵgdj,չ$<43u ^ҵ % endstream endobj 535 0 obj << /Length 2154 /Filter /FlateDecode >> stream x[Ko8Wh=@ç(-4oӝ=4٦ʒǒ;`Yӊ$,*Y% >??W!$u \@'1Wӿ͹l´?ܤ 'o>y`)@sU`9+ (I\^@*F_~?U:`$2˲ؗ(QL#PEeQgެ-E`i33Y<%Ø]0dඦ܍QL"&KB7KF&HDZCH.#[*-)x eYK"a0qP-~DYc!Z?@/&I#QQܗXKck(!ܘ$I fQk2Dh%jޢPnG¾ H|-5l[dty'3|+ؚhVa!23<v9@$='"dg}^\C\{$l!װt\I"Q¯nX6v@i~3b 0EQ^j<ۤRX)+NQ0u4Nf(~ Y&jDn̝Nm b偨{(Ho&=%\DX~S@%?nC/ɲo?S]x7 kz{.ٮo=}'N{.^ٹp!zU{K^ly&ޜǟXRW 1Lnj0lscgEY_a{5c*V8#-V}}g44}mlc<뙤aIå>'%JwHh9m}t .}Yjz65NpyF+!ѹ=A52k c%~H]\mֻ-D4}˩Ht4&S$沽6[; 95VoOeL }uU*;l9#զؙK땜e8%XB)-:aKt`soݡaqn>o~o) \Yhen6j=2 s\~f;1 o hhkeO-0뫬̀[GȪ6}ΛSr3)zegzM)%}vۦ'{`7x}zDt;XBty/ aQzx,O6 AAW# TEq/-MHw9n7&UAKF;~2q?{ޤVH>vw[ DqNɤ&(+gCۉM4ٞ;r 1M J a%`8Ipw(]/Fh^6|6^_e}jC*_}D5`߭~['fiwټe&=2F@z68kz6diMɐwB@f@v)g^WO=b%gC>'z&쓷x¾*tDLO?y*|]QO ct 5D_A2З#[p<#/OE? ^OF_<j~B_%=q2^k&?_hAX'Kf]_Js_L4;j_> stream xZKsFW`Y{ ѼPJ%˾QNvJ0 YU@"i˛A458 p䧋_"I*@@E!b! .y2*LEZԓuV}qEN~8# Y|s 0bQ<^ pz-&[N)x%1DY?bIUwU:b|M$񍖤1{P` =zFF-C$ $xu4Nd0DJ04`p"N-." Xt3x ^Ɲ;fEwdr_;w}yC>f\ Iʕ"rkڇ,l}1;&J1 ( 8BfzrK4i+=t#KtGλ+3`N 6X/MaWD(1mӘź6  ( M]sD  TE?ꍦG0`z5|N@G jmL_ sǜ1'\9c$e1|m+=o D}sAmNgsA@ahJ9w/4' `є Fz@;vszziSqB8Ӟn3sH`;- :5aj\^k8' [/n<"aĸ\-D[s||p<.H¾UiEtt`RTv^s cm\,b^YYQeٵ"Ҷʅ:(qzoҸnV)Җ8I䪿+-l6Ul XI] 7YLW8Mm}ЦIczɀ}l;yG]Nk-8ihjEڵZ6h/>&ڋ!/vRU~S{L ~OFiu<ɿ1IC`*BmDzM vو߉yiŠ= =R`oX@ Q4BR"6x}Ew&,#)m*}nf"zmB7:=\ MȮP@t>%1z JeG{jsq*8f# :#+Ji5~4p> >+4WHۻnX{[XyW"^@ 8f#˦^ldz^b4 ,j5{ӶːN[EَPp5񥖌:QT:?Mi!w֕U]^g98/tt&?4|X^_v a]7gïn=wm6l[Kȣ{xl ņuf$!{e)Ϸ&m n'fO|q4r=A_Fzp 0PwoHG &"zpp1(pB]p=8z KĮTo:ml MNrlߗqFU8{Yw|vo!rrcn?̅7AB=h|uotv83wϏ͋Pώ3_g3xݓ I;,CD$ .c(R[\׺USZVK> XY)I2^4}!7XgK}۬p6N&3[ҮeB)|+2:q0Vr:+uPג4w2yY:cS$-!J_s¦=\ȸkLSA)ܐtt{[eIVWi~:-șl5ӹm1D|Q&7U3CYSCTVba|b)"UG96x9ѕ`ae "<B =s.fq84 endstream endobj 543 0 obj << /Length 2804 /Filter /FlateDecode >> stream xr6_AZ0$q:2NuLh[nѥNAI[&dNH88 pwgG'?2($eCT Tb yo00mg|3~,ǦM2Mub(ޝ>R8 AH+GBhvtch`TܦfP4ѯG8c#BWA z'K[FHİtTZnMh%BFQKą^h*FuJ(4mpڇiNѶ.Ao.8m25QB;^a/LQ/Лʨun0,8#$@UYOy^^5`r@(E6X`OdcWr|`evBxb<[!Foc'eXc |LOr/zk6OI`VFPC0fy,~ӈT#Cl(*)^䭕̑13G VO(%̊RZK6EO6(p ).,|>L/J. R$CG/FpPj!dL xQR`0y]@",`8,% Aߔ%M,G/_,dgiJx2:v0}󪞻l3VI5C:"b 3f,p7Jl18u蝖uKv&yūmx":-&yqinkzDfoNur^kS\ 5y1z"wsb,|nyރV%8k}S {sao'߁ozϾ<D:#}~ H:"ˉ}ys읠^qd[`U.9te

u#C6ft%+ROZ&+ڊ& nK]ABہ[P?wrbN(j&N!rG;UC创 :vgTtWͧM0(TCY^ H⊾y˛IQ?+q!k9 [i MNA6hm><%{wJZ|Jkg+=Kf 7V;k'!)mm޻hl1TeSgiy`^50Y*XgY=96x WVkިx* Wy:c H=E킫JBUCT}qɋw~3>3/TUHQ tX:Ea锪KyGNJVVL^uX*:qq'SW`la|HW8:M_}s 㲶O(u:?ã(+{SSj&O1OVڷVbH }4c(lZ"\bvcgP!@/i$gejJM AGT8hSDH{vi\ -BT 9 endstream endobj 547 0 obj << /Length 3048 /Filter /FlateDecode >> stream x[[sF~`][Rըw 5ZV2q &I1`9=NIǏB&LYb>҄Fk>4im/rob59ƫ:|׬|cp7*&w,:}EȝЉUVlqSCEYKJa) F֖(^] n (DaEMǷwSG i&IIv9%'*TO?;P]69\c^Ckp-pCE` /50cRs*Ah#(cK ,.B EmV۝4;G@2|TؠtJszti]g`~]wJ/k r͝W5NIg4҃/T"#*ݯ ~kJj{nK2&lJc9+UK(%8t pwE9#AKCF"&! /tto!0vlXt()іhe, ʿ枙+702fŰ1`=l@Cƈ@#iTee/vM%#uFiY,>> stream x\KsWpIS;UsHN̖(KYJz]4|)>$aWw?2(O<&y2  w?>nTgKx0YfssX7T ~e-TI -y-"TkOk|RzZ |FP ՃBcSq9{̂# pp NgVQ6q팙k=`|O<; yuTbh,DPmЪ!OZT^0'V;C:2 + ( A3j*1^$A)u( C᛺N ԽkC=YOwa衰+LO9q)ibPGcP!R #⬪Q[*SiuݖE(u;+xMw^lm/} Ph]u.,:.dW(؛Ǎ^M0$D aJBoE7^_w;zJJ4"`V_ub C4Jfܓi"zaN4n,Spn ;Hz?F¯ 4z8 h U OQJSt &qR.yj,h^@m(ʱƒK(FjqC-Jz \jvЎť9k1Jj,~P@`JQp筧Cv-mU5oCPʐ72hT<]-,,64=z*NUB-;y7v(RTWQqi˭ʶlΕmޤ)I֩H֘r}Yۮٽѕbgp,ˬoml䆟>@s$)LR;mC*؄H9> stream x[s6_Kɛ&M2\箩;v}J˹:0_2uxU-.|wAAb'g)blB ]ח4տىaMxK1r4}>yv ;)WlnM8򑃪*8V+qCaH;$79Qk”!S`w F&4#ȗ@|.j"7% @B`kbI2=Ԭ6ΐm]R. nMY,DGQ(7IHo[\Lun6p*06a ۂ֦1āinxGyx=5z `bݡXRI,aɿ_X$n@b.dI_={3 ~5(Ҽ %)`z0~Ӱ@ZrHa6,HPZm)+nJZ]Ewaт{n, C1o?>^d 5dnm }\66M\I@ǟ9@^qIi ; }>Z}n[}lΧ6S!}pkHI C<9%|֑t*\Jӆnb$qrm[)$x9P53#d<#R1˩S Ţh T N#4mmP?Ro&BhadhaD{J#D"zw{Nb&U!+·w 0_5BZqd-cjB 3 ~H֝ܓaV&vdwVԮ-h5\(;͚£=b( :c$+$66FL6IjNT&CF!*&2| Ik Yt|g&Ӹ`&V%8x :T~]!,gtkkAQwjzx [$猋pKC04eͪ4p\ÓѐI7|"*f5fU8X0@$=渫Wg`dݝH[9w0yY S4VleRj(7i;̖@f"+2G G NMGp'P#:y5V-#|JZ*mBwetF)JCe2c<9 ۣW: $xWrq. Uox?^/V?PLw =Ă=Ф+*_k&ŵyKp,֜ r4l7'ް:яY12 ;'Oŝ\Ht[-H㩀Ϟa~=Pk2#q#VjvX'7 #W=Y;෧B]Tp-.=:b8龯8bVӒîvguy0=rPXu@M9x5B-z 6&Ҁzaf'Z6Ok[NaeKf5%1guMۈ*gSw,gx62UDSu SQ.fd[:dE@R篱7KY<,K`6C]fSn*XEvYcxPp}=odq)'_:l";#$ҵ%2(vh;bRʹ92vmJ eUYgSo-ֺD*%K[)&M4 'ܐ)Ichv\J{:jl :C,d Hª0tw2дvn|pvF#i !FL1Sj7MmA-T4%O bY^Y}1>h1yYz\qM.u>C6G=| c|{9I@EC"~5 { VuL- ORڇ8?oa>zBDS.JWO^`ӞaڶjZQRLwnzYC, @(H Y[%Kܠ&V5'DJEԓӟ._̣/U(&O!9rfGyK?!BϰPplAig[뤺zkv@yshf%U 'l9}T O4XW{?yJ j~`zX(:SX Owֵ ÍFH<L@=C|rAY@nI?p 8> stream xnF_5AZX$@>%< ZmJBQs ȿoӲ%QZ;``}TU]}7g?\]f μOj&J7N=}{fqTrB$E%a$JRcj:0"6ٳ Y}I6N ϸ$͝VotކQbYі4lAX+eT<=HOm8> 0g]AzVﵶ?2y"l?DwBQGwMpe=|A߂_:۱/fΥC$d'"iί\7|=_-b jT<$&yVx;#xtV&&XP0{ F8ʧׅwcGY^J`>?p]PwY V^ۍ:|8V$(I+xF5vII\**cS lLCHexd6iIjd'^)=6DKBWٷO9@Ǒ[474J`k8P6@5a!:)ޙqWr+ε>P7x 0tPC_oPu4-M-ש +IRH*ŬDgZ̖Е j`BU7^MgQ w2m/ N3YrOghlG>oLi7Fiv2nwKQҬ܈2];sH\dHR[O0`O y*{9lTw_X;'FkT'6y!BB3fcRܓ)&D3Djm&.f]Vf'P7e ?^]؊lׯ'[- B{04}GI0 1>&tUePY>Q/7.y_,R;Nwb$GaH^ s9l-},NbE/XSFՑ̫KO-` $8pĘ<Ul)(Wj?P>`mDE ġIH'`]m? g="1f+IUK;R?4Toapިǎc;"]iX!0d{y6%aJ·) (r5 {G'(=tֈ)}|&̇LtJ=p=Sw^l8v?3$8r = )"gh> stream xY۶}ծ`G'L&-xR#Dq3ps/+>'oy-AFRMEe4b%wm?_3R"_HrV,o}<6oP$t曻_n@ r$JF?d o7jpAUɿoK' ŖS DIQbYΖ| h N?nBO`{rF5XD&c~xm鮨Ia2Pcyͱ5G^Hq 8Ҷ0 A3`ɨZRa2n=1v G$>nao^, @T&\9*﯉mOCRҊC?EY*UJ0_a$ؠzmʰUz.A6+=q m#N*ajQ3D\[Z>)孃*[gכ 48alǡHu1DX_&X|=+ܔ:, tH] J5%)?_rų❼ b|"X[@Q0QFeif b7y/W 'YPz;sYF (ٔAD|דbxט΀>a #&Bٕad c)ȨDDR*V>Z\ C& )j7ROqqyC2Mkiq4]!BP|Lخ\d\ \Zo>xKqUSp/GEGɹDn̰ ;i倈t-}Gee>fyu T^8W )Dh127RlgMk^mpkQ;V9OAs2M  q!t 9Rߝڊ59#؊®E/ d!CT$F_Co xW;wbU{n@ؓ_b[rLvгivQzPD!]Qp%=v9\o>!_K0|2,yERp×u*Fc#iC;FE8'pAQBHDuG1x8zn+ƨX>fh 阾>3q9\iO7 N/vZ:ͺ4oz2pS T*/Py{_R #'UbFާ4dJ >[m}|kywK]fOe̳:?\FN`m2eZ,_9C"[~b;o&M޼)_7o͑M.W:.]gF>:+.\8Lΰ[ֲb_˹?< &>rg DGDq?xJiug~9ڹ>Ŀ}}w`.?]9Dw׹St&R; sD:pr/thx;v -CFHZNgD|Dr}:4nRC[(ѽLW0+DMŚ`礡/qu=i$*%`8'b_f}CATz78,:wݱ4˖e橦¤~%n3MwXoܘtc={1c8ڮQP`}U/Pf_ya "d}.tYo|FS߬N\#[%e4ߎݧC L0lfjG{Xl3 AJloi@DIu6}W~!)xN]o! }xc7VkcrvhT|.ډ(e < i pU$9>AbcN wܺ^cX> v+r: 'HY]sl(gҸ,0>8C;:oxolV)(!$ Vow ȭ+4y[?l$>"-\]ڏGg譅RӺ.>*pv 4[ |$7A\ 8omizc߽t endstream endobj 571 0 obj << /Length 2445 /Filter /FlateDecode >> stream xkoFa(H}/h 4wN\& DD$գ>#cYmYQΛ<+Hh$F?:8} 0ZWv( $o|@#- N0Bq3yp2Ntz"'Gǝ'㒚9 )@ߣ i&Ύ>8:iЉBg)D %CT{Govkӣ* z[x#“_~%+!F 2I $CӃ]ptێ]pa"rq6PlkzS ?}Hx)2D=!QU# iGuz:q/<2q{i,uм}l{ g=?2B sN%nX lTQdE$KB'dJTEA,5N4lm ΃W,h sE._ F|`W³MH #8 :񴄘ʕeW ncoPZp6pp aϕ*?$ÈeY -BbjʅR9.ܓ\ikkDh90FQęrR-2 ;czD#@ ?ҏ[M*Tm^f@Aó1v58"(bǦͩ(%{yg NDsX?|Ww^X.}".xXJq>n҂c8yKti8]CEѸEлE.߃Yڭ]R6zPKtrܰq疖'ciE0&K5#vD\A(Z-_^g=&A3к1h|ZUR8hT VmGW;9{PuW]$Hoċf-cU5%P흭-+{) Bռ)kb>j$z<mhzn +Rxy J3|N]"7Um/ņ!\!ik%#n-CT@-Xt[p_pڊ.I)zp -n[ !n- ^‡{Ǖ*?i"r[bt_ڢ}=EL-'/53Ɗ{M'mܒE:?tc묿}Քcɴn5B}A@UbB/> BX{lj qI<߽k^2_ƸK6%` pu]x8PLCu2t;4&}Sl @FH[,Ob0Ob4_lkم+@q%-~[._^L_KSB00JnL)aT )c}K1Kq/2C@N5]+oPFӡ9F4߯:ݧZhD1ǟO ŤEȵ$q{"O{y@edӥW:t-\[z@9y!%╸6Smo ڶp1;}~DܙQ5 j-P,w.D&5!#2uAS^wwBim_G'E++vrӬZ]!d%V+{QBZ6J^|lfdO滴,)WÚ&8R5(x m @b{ endstream endobj 460 0 obj << /Type /ObjStm /N 100 /First 882 /Length 1719 /Filter /FlateDecode >> stream xZn6W@rXn@`@[#^$A]vF*kkWr!9|8|KXp.299R5:x =9i+\pN %~běBY2Lń~mPPT\QKz V U3Vqk*RBWWsBGjp$Wr;:&FVθv{TN0ԚPV.EBq 8Uy7K =М`[g`kure~;x Un$pL7b "ߛN1[ia7,;ǫg4edV ux٪/R\zaYc=X "{bQ4ݺXsRȜU0;v0< UwۜNUz4:w c3#%!mВK&LS*&tl{BH*q[SPn6ɍc" =۞3յMhb iu#t! cı+SuRY؞MGz8H3>'VΦD=%DhQUGEn<=mN&~[>o\oE}MXdەa*|| y52즺KWI 2bꋖƢ\KTƢ0Prȁr ‚HԱ@%Xeρ` @]́K ]W>FH?Ev)ZOr9Ѥlw4y Ɋ4/g7Kiࢋw$CYط%h-#KJMfw#sZ=4,i__wW?4Ǐ?'˷͓ 8{Q}z/(vdqvr/>\sOjȁoώ{ o_HT{9=^ag#'8vcvqYd V\G&~e p y~,WvpI endstream endobj 577 0 obj << /Length 2374 /Filter /FlateDecode >> stream x[[s6~z X0t\ϴMfԝ> -A6HT\O}(גڝq'5IĹ\>Ի糃Bz|{g3/^D;zdi>A'Yzd?깎W>p>|/ PQ%QQM>So =JDz7“p?~=p@-!s\zObe$K$])`z\ &zZ l3d JF^]Hgx3BUKj;' O8U$04ʐ*$K/)xAx^iNgPCr|g);^ޕwW"'F@|.#EhK!ܳQe[,-?FK@PھF~D(+[EjK1 />LoѷRxZ=F1`•%y'Cp/F/Ա,d,¨Kw|ҷwjț#x;IA9Z-8f"honRV$jIB7h g?̠yCcE6C2KM}"*ţ=(G-apVpj/Ù f4Dex?WMަz[yvi^Z%s]v 3ADyl1ThMbBhEoWb%|P*l Sr%j߽x Qy-.2m0(:Ut:^B&#WȍD6Z>nŀ(!Џ-=$Y~W]Lf?mrfrܑ+xThto[1b~@DJgwSV6jRp?\" Ay|\<d;:l#17[Նz*zDkҊjoACUѕV,+ tЊ-|{@g(+{-/a)LG_\(N tiFD;Wݛm8dT9f-a$7@HA6'Jly5MvqrCw`N8I-?f ?nF]ۇ %>噽P?smc{Y >e|L]l憹t i"#Dk)Д,P5€$a T'ΑmZOR!hpv8'qjog,uw(-FqH6MzJPF0a#R1ئ(X\Q;sdN@B.Z6n\[\ysUV]h3Wm~E/N#ũ|Y Y}n@s/.?<û-rT,O|j/eF%7XXR 7@}&Y>لuC*ю{e1~4 {o3M[EDiN"*UqRϯ]wD 𠕽LЙ;zV`be`F, -1&edy6O&> stream xkomW.gM\Q\=$h E*"(;\>ؑ8)p3rOG?=^@ɹ|Ť0w^^ ٟtfgIWc̊,3H09wGB jǡ^ν%_x8.ͨ}d@^z- ?^D,@Ly00F5anE)`aFAJR~lZ@yiD1|T ޒ=(5]hwtޭǻR( mޯHO ᣓ Q ~'ǁ‘"HOA̸'_#䵱-Ł{[O#gRrmr idSԴ'Q1A,1,VNVy /fyQ`Km4AI7eFЫjC 8hrM~wj6//h2oKҬNJs.@:>ήi3Ke%A@BYSaJ˺LhuED( շx8xp`C,;.|ڟH\C]/W Xe[^- &%Aw& a&5X{:%pc7L4k6܅- 7 Jf!|c@RnЋj95$i2G)bf.CC dl"رF"at~N8﹥f?Ʀ/eigsExBi"4LY܄cFd@\}XGڝeI9ΔbU.t&>- p[4ZiirM; `d0ȗ02ӽf UJP.>e\R;Gk~8Jpc<ŀ m!МBC|H$DAl5u^{x+% $@F03 AxmF6aQ;Oy:f>g0/-K<Ξ)mk,Ԭ Y­vx ;M_.Vu-#X]mtY:Ͷ]OI5,2P/yz (g34Қ?4?r>G&jEA] 8l|V&^amR7?:GSʯ4N&$X٦8\7@Uq+Ñ'h]u<[&@[)JVVܦ#ޜMu>+`Ov ?{u'bҊ1Gf fRԕ凌QfBDFĜx8%S~?p3A; Qj AiNaU+B}%yz4O~6QYKqq{vfeX|Kp{*` '>rDrr~G+t~C?ä _k4LLvXގP.m1%Ρ]C }@g9Kk|J;sg-KS9t.ݰҕJ}edTV* n;=`NZ>&˩JeVgP]SÜeɻɯ/f̢d]LCǺ+؅ ԍT*CxAtzYm%*bA\O1"Lp2L )lSo%ӽ^ReO ׄ ă]qhv9;v,)B .o_Dl@:`c%.KTu+^]A8"=5MNwMM:q{ܺ.4~WUy_Ty@y'.+ysЗLq} ;!N8'Xa \ߨ9 Ӫlr7NtJJ;o2q_&K%߿Bt|/@r+9 (G endstream endobj 587 0 obj << /Length 3271 /Filter /FlateDecode >> stream xkoF/)P A K;MQia2 F~3;˧iٖ,Ź>ffYMu3š Wg#": ^~Ñ_jΊ b2RQIRE~:x݁Ux hVLl<8T\i`҆P΂^p%oAbBu`CΌ|VLf2|pOXCi'U[7ؒ0 eBGG? %aBP+ #5nx(OF#6RX&OSZF B3 yNwK?gL;,y.vp$A(pH,J$O.k1|-NYm'zޭ&t{ދtQ吊I^>)jO2.ʒS1{8fZV9KZeGW6rqP15귡@' ӊt;W*]3$6C+\ diNg ГCc[d GJRƙ .2){Lȵxn=p:MYtgO{aqlJ4eFJ fi뒏z h>"Jd:&GEڴH; EsB` :C8tɪHiI^Y~WHs'Ry _2qn[0R0!);=284'lITߋxztt5=qIi.3KNfSo-xtVqRwڬl!ɸ!(4ّw% ddW2Q2i@w@k`/;˔{g]"flԹ#Zšf]ԋ Cqw1A%¤JIq>_~{8Fql:w.cJN:,ѠITk@XܚʲXGyzɬXCp #P௃LNG{H0n*zG?cBw.#U#ZbbJTqb` 2>8!8nDt8 U#$d)pDtتp'/݃6?9B ;V 2 6oʈŻ[u,kIJ{ ǣ t|gTUŐ;rwnpn*;0FUT\,vBvZj/o(]XAY㲥A'T{q(&|Wmz//]R5M[kГSkYTW~l-&Q,@KԠ+"Ѷtm^ FŃBQy4l&-{fK'D590H^ܷ߬!"ShhS> _،iqy}&nGp6g7om ["{$P.c·rU9ftSW.27z(%F",B?ϓҡ{LG [c8`N~i,}W|$Zpn׈Y֐ endstream endobj 592 0 obj << /Length 2123 /Filter /FlateDecode >> stream xَ6}B蓌 PȢm<h}jVBdɵn;CRl`C14xٳE$V\@HA Fo?\*ݮe4yU~௲"KU8Qxy_g Hрè`%"6 " E/gԳJG,@XS,GQ%iTҴ*f `KpVd3.ÿj[{H!1yG0.8~7CM^ܭDbF`(yW}cxD]I5]+C1V4$Z$cˆDjJpi_ףӏEvcZϋNnuo|oIX:Bhx ;|I+JyCI4Xdԗr;7W1~9h҃-o@31lf !}yIξ KOS}Q.q~;@_8>@H#9&[;!b^1]Pp|`$s̋b0;Q]cxEm\ȎM7ϐ) hh\풻 Ȥv]#So~*gsAy\+e3!÷3FdvV'b`5ۃ1N&۔u|; +Iɥ &EJWF,aUsJPYMfE'GeNBUh߹ڈ3 U|<1^;?`4j#%WFBp?zu'+W߇L,x8iMuyguu9)=vS]:+Y3;$ҤʩpY?D`Agqk۶U&1 9mc"qӤ`ӻip,~C:c2}ϞjCҁ`PSM/Q+C~q@VnFZ𘨎*QN7z PX#qD&*{y!O@2#̍9Ag`G}pGF#?ӭL8bŸ6Ba;(@v'@'&)\PυPPEV\ /]p _Cԗx:di6Y-|bZ)Tq|_Aکko !{("uuR+KY5#0ϋcO:G m+E+XѸ[UU U8:v6ajD%¦Bn6zZ~}pAck6IdD7!|pJqRqS; l BRfs ;?ZbA~✷-my`?\:lH}ǨwCW JCFɫnAbS!$$1@E(TlI]+γzw&wAoЎk$3VWy-ʵ7P;aD3X]cV+v>'K;z7k{_bx ^9ZLuʮԸ~Qeo^g:*&4˒4oHw/U]'ܲ#ғX%6)ms8H_=vQ͎U@xw ~6_ˡjbLJP(j= \hprP 9lSy8]hҙ S:[QQHZ먫7=n1ςY FJjrs۞1pqA4lߙTDj N;y<Ćj9[(M5Ⱥg7 \iVk2ѓpGmJ6aF'2dI3}T6EǍ0DLS<4)zdVLg?9$?^M V3i7d >|2ߟf7߮?Ô8'˾H$*hѸdC۷XgfmPC?&*r/^ endstream endobj 597 0 obj << /Length 2693 /Filter /FlateDecode >> stream xksܶ~'әe|^|ulwIO|3$Fw+N{w(:ž]z?),d^x8b*Ri]FeLѬ7*ixDfkPCo/O? ȁC/ݝ{_z8n쨝}d{s;*_&Hdpf :fJcR!1Y`"ۍMee6$,!Q*wޤEJfZnLq8`6{ؗʋ|K⫏JaSjl }kᯚȒ*#Ⱦ*/WU#H666mʸyєnv[Ȳ# @_v $ :iP@(/|GE{(iج<+`aFU-GLK`K)JKG(da v~ʘqÚ=(}cۭCP0w^xu1&M3O$ﺤbzv.KY\ Q;#k`d:ߡcp](9x4 k䠻 J2-n%/ \ÑRQ7&n]Ҥh3?\d%Z Kw)'V}1"W%$ E4ݲw\z* 9!=!( ͞_;=A;ސkuWkN ~ ~w1"4DDIB7zC[`9|11f1MR b.θqJjL 2<~]\Ӹ[-.;IfGZnbR}BEPo3G7Q1mIn6UkD~~gg['(Btr{:@&eߖC;4U dPR+S\uqR\5rHq'ãJ|ENx9O(WIe,dOePx$|P$v?'^@x uD,amςXاRܝkdEz4 wz`Gwe5Dŭ[ {a}WS @އ&gGػpxLN[o8s!Ÿ {*|{LnTӪdVe̡!ꅌ9T٭Xb% U ɔSI.ݓ#7ik`$!'C 9Cznp. .-E0~0"ra%JHXYjv*^Il;N'+,A'[[{$W"1 nj`Qr\7%pH}7n WeݗfwZЉC)A,[*-ANA"\^65]VRT` ]I&tSzTNX(b\H|kv+Ӵ=Îm<3むΆKS*qIUX y%{2*)!zT=.v+TGjZ%?~sGp5a*$)ҏ"4&oo_^5P,C5@uv§݆JhI}q7hM4t1Sj3 ɓ-2T6^6[dChZ4*v@R;ۉKB}Ɗ+ |=0E3HCY # ?Y6pIlk͹,-w~䢫\[9%Uo̕W'*^HE! Z=ѧ^=޽%XU8|lk9oXm?s0xKB=jZc+UԖ']a. .'!eQnt7ƀ`|&VD]j e/5݅b緝ҔEG/'{f lmGY*2 n<#$`54'T; 쪯C~Jk^TVh@ǫPhy[ia2jj!#z7cp endstream endobj 601 0 obj << /Length 1619 /Filter /FlateDecode >> stream xY[6~ϯc2S,vvaY`R ,<88vNG7][a-G禣|[{ػ`4!B#Fn'Fq,Zj1N sr>Ił oXɼһ>|هs00A"P#ϧٻ+%:!4';iD"@4vN/.n|F_\㗏޾">Ex:;ZCF6r/Aӿg O˃|܌K3N#Ϫʕ}v$O3ʧI'\gDqL%A;ՈTJ6qYa.wuVAL̛(ҤpU*>: w׫q x< QDԉ{leQWF Yrh,6XEq{~Pbݛ B="V ve5a3em )uz,R'fdH!BhEt.ATX Yan1٪=tSbW&svBl\qbߵl eIԙ\FwzJM [odZEh.ʥӱC A0r=^0 s9ǧg-jn&4PאZ F)T,ɢŐ%CjZL"u!*,X/X٩OZ1@!rD϶+ RaE:yv99!r<9eSڗm9֖. 3!?r#rR]-Ŷ" Z1[5c3rqXUOe;}]п݌Ա>k.lj i}>'զ@ZNK4?&  Ѕ(X?/ң[=[<3.i-UޖxwtAeN8u#D"㺪Rm(t&"AJ/a%Cݜ"Acnn:8g ~zj[O! <7q߹5E9}&-r)C*R]}G"h;7l]G c3 A48“{ol@ 6g!}XaJ3N`B IxZА΍jz%/wk2Eh)*1:op(6]qG$=b&8L6x endstream endobj 606 0 obj << /Length 1841 /Filter /FlateDecode >> stream xZo6BebI!iIڵH5sYP(kTQ|aűӬ|HǏǻ)عrjb2z2'B39QhH9w߼q4 tdLjq7"ER@bft4}2ģI LaB禞t'ίw#OD@H|AJA^l 5z(Q@)Ƶg{c\BŎ(b{6 +x$(^o9A")C дhGpC7P|n@ÙNxeB>mD ᳠+T-W`鎠ecHp@ƒmJ^$ 砟BODCݔ0P¡qΜBU"O6#-Wފ1^=%8UBzq/L+ŭ'\ g=?lyGWADF>!/'ůTc;았B AwsMS/γeUZ٘a(]eB[UԪy$@x:lZVI$6:M1e@RcW H\ʴv`N[e$4aZh {b8!E"{*]z2Eu@4p7s=+=kYr"EcRNMX8#iʈ}\._q?xVoX X@+)E`c !ևAZ .;[T6uY\KW˸Gq6SQ8_Km2H0} $Ę,HCC:|eחU;~%E.o .~J )İW}#jkF&& 2-As/.̫4.&u V{I3nWAJ% r8xVea$M1^T_&@!<`XT)VPL0lTNeȕQYhA)Kni>w$(f{ !UȺt8MCtjޙ~doL7XWn}\L,ٙfeT:algn>dj+6"cB6`%Y,ʩt~TO w Ru5(x(WQ`V1\#'`!|SHSHNRuK5(fI+*7v؆LL@l_v%F2 =fVÝ 7-,.,+,mJ)0e = 4&ݏu$ _2dn_ڻdzGIR_LN>l-b<oL(V'P76rʦoyz5H+HunĉI$J3cVKRhأ8r鬞 w#]6aUް;5 PVu Eꥎ ³Vzek@fB sܦ&1\j6S{ ^QJK7܁hgN;sf>YA~1ތ/!$JcOs`p_/ | endstream endobj 611 0 obj << /Length 1930 /Filter /FlateDecode >> stream xZ[o6~l bI"q}JkvmPdf˩$7 nͺK""xxn2vfv^MO^PH$'ɥC9E/DL`x8-F.EJI< 'V!Uh98;_;Q8嬥8A/p> a7Y~eEC5;Oɲd.qqcǐľ$zZ p<,qf#a3ݨ)cKݾ1<\uzf J?"&[!_xq9ͼ@~ $+C##&WEX(~IU%Z"Ij 5;\4 #VT\8pqv>Hh֩E?\(`oԿ܊Yę=$WG2L: @֋7$QJcyb:Y, En4>,^oף>@1|"W53Ͳ(f{?zu2~>}4Xin$R4!cT\ٟƗA }Yp% B)|v`T2uҡS~lVw@zD4$ #`/$g{VCЬ:jG;kz/R|l@K $QxYiJp%@WhvA1WX2̫+e-Qi62w[І0O +$Kؔ(uKڑ# TOY?pyФ윹sJtՑ y5i'O/^ϕpqEgb UX ϏOƟN~m -]%Y×fujYyrm|xq+壯{☒eykٛ!8u=8y6Zdz`q!GX TȶCTy})i<$cl_֩Fj:ml-)ӧ#bَ)\Fp,Py`[I٠GT,ӎ&p$4r3"C3{,!Ye}tp3+ht&yp8JrEˀNZj.cjeTL;XRZW3 `"8; BIöC VTxhV,.-7!'?ܠF2.F2.F2^ Tg5kzw*7? h XQPE&J|f >rZBȭϓe|)7;MՆje;+UoOߘDтC/l!׳⢸9/"VpWq2pz<ؼٱkf5|u&^TR2ի n7h'/<6c.PSڲ8]=UQOvU}] Z+2%ZcVPkEŌVᮊ BO & *}~ 8D endstream endobj 617 0 obj << /Length 1990 /Filter /FlateDecode >> stream xY[s6~`ߤ}rm%MꦩLz24 [PJRq5=&J1C~<8ù[^|<eVBxu3{&~0(vgQZ*&yoO˓?N8[ʐV>V-hXrb8O~9Kܺ0%.߾p&# NSG0h6v?$b&y#e; ]WW9qgԝɷmVmz\&ThtQZT:/ړ& [T߫?Rv=jUy&@۩YUBR͛Q'˹}#޽8[.޿xuܧKV&j>JK탱QN\H,D1B;d<N~REf)3TWt:oH 6Q]2x]ȥlZZO[ 4mmЮ՛woϖ?%1:FIo7IRID0R5^JţRy 55BGYZe&.n}$t;qPhkԮs\EO ˳fdhi@xxA-(?" SplTF1N堹M)-4L1WœMKWE 0)A+y l?hU8%pc~禟G91Q!Z&_Z\\sB_r<@#O+Hv= 3ioE?z*TN8HM##i<$Ա|N&yC-aV,0y I\du/ `8;##PO i`Un[G ԎYM coWp??yF(WOIZ{N3.:J 0T-&L0i1]y1 ՌE!t d&%A@VG(qy}򓏱nl7vO >J"m&-&q?P%ї5@՞r$}٣5s݋W " FfX6]/@C- XX71 ڢyAl07wz :.! BE@;$3h69Ѭnϋvc_`)[  PyhSU뒌y}Kޠ ,3 5r0\!HK . N@h{! &Q~3siib ֌R]] &]_/oM {ן~|{vKVP+1"58*2T!Y u8۷7nFЄ[GX.A~pc':wTI|Z /\~6{{4hCq{Lo}zފgjzIMm݌gk@;8pK/0gx{m`XJ,aRW< .bD١K` 'A~.e3vYV0Wi㾨#]A&,|)6LR8B/d۰L tؤܐЌaQ>8N3x?Qؐ!z|cBcBQV#AV4o[<-(?lC'#(&QiQ>LH'&T+a71R5 F˗n?^Lr<>ŵ_ag^LSCx6qe?ޤɣ_pqS&7;_Eۈ\4=Λ4_!v endstream endobj 621 0 obj << /Length 1514 /Filter /FlateDecode >> stream xrHUhhSVAr-?of$˲X!Zs`^=n :/o@3ɑ6!!Ge/`:쾞ǛY{Ft=sdD-VwA[)( ,Fs=$h܄hC͐3`>E;:?17EK88s>Aj̹vIw>$!x>REIE,F2GIw4Iҝ/8tFĭ?dqd-oݖ'MxG#x

Â4)@rc iWU8t5 ?bB ~)-v*[)$HDC~;Ta/YA*Ac!@L(PL;X-8*^5Z0PK4:=SЭQ jCM ) LvGYݸY%;mB1 k-̖/[lj%!$QQh iQ$]O-KZ{ uYy`#SmiQ-mNRśud|Xyzjơƒz0ʠX!{^ھ#-c-Ioo˥9Alu1km!8s}jmsB[Gkm!!u+%Ć ūW`gx\\K'YsI~X. 49Ҙk{"o5M6jNo8}=j ב 4ɫB|_˽k>#ލ8i<'77 Ź'Y=/,Hm|rgHfHH.l9X.ʢ!;|dG淋O'ɝj޲pωJUhShܩF\~6`&n(gqxTTccD&y~!4ѣaDgP~9 ddkX/xRX\h >J[=Pm#ALF`SdN'9*,ѻ忟/(/$T~|qaJ*N^~%GB@6,??D޸= endstream endobj 625 0 obj << /Length 1595 /Filter /FlateDecode >> stream xZ[oH~@>`)L>IMjJ$fkZ_gnvWp v^\dQHI*;ǧ\Շg}{:8=dAaEuKÕ#|'nn3WFLβ;\D} s{VK\V-m,o"Ў V+T#Cˑ hIyQ՛pg_r*M 1#)! aOӰϰ <';>pR[,>gv'SZIvob5EbHBhe`!jhKb$(iE+#q < n[ۦ0m ;}MSog{q&8c% Xҭrdoo>>?麶"}|떓i}㝅o,}Ĺl9MIK}nd _<Q3׳jrlZh0;$@"@rWf;' D M( !}UcIKYmEZjᕼ qEo- ,s;?Jԇ*) j)! E>_yˮʋz٬$zVlH+uhMucJh=cBO* d&@nOva7-srqj7=g*"O޾ys|YR6VunJih`dӇ"5>xfC(=lblpo> 7Lv9{ +wԋtqB44\DeQlY NzZGmQN(‚a Ӵ Cl,i@P@$#ʫ<sMxva5UT AU[nƾX ٶo>_lE`\F]8v9JLJeG6sѿ t~nnߘ,Gݎ6Z}!19׳SWPdT|@MTP?DŌI%߈H݀_]À.;%tω~ZY4$pvg,oUW }9CNaUW|m/&oưKp.-~.69Ď]Y됮|e8nl5Awt@n_Px#FO 'MP;@fЬ$ƻxy }4D/,JnjC` ]?&;5ƺ_{G:?m&BHdN?3ْ߱+?lIf?si+7  endstream endobj 629 0 obj << /Length 750 /Filter /FlateDecode >> stream xWKS0WEJ pL16I]I8-  V/qv 3oaPDŬwb) .慴nyUnr8Vˣ~UͫS d>^d!3$[V.ј)_0;Li Kv:vArFFG[&'4(md.)1boFr>UKwp嬐ۥl¼خ=WHi1?]2* pVEz`kKc !_ FjriEX&('ɀAB5#>*]> stream xڕ;O1{-m׹MHRr"xcIXy=gp֑nu!z+-9Mh,<]U>HB>vlS !^P$Ucwe1yNy9Ye'!T*ar|ҿ ˗2齒Y3oixvN%\2U+ endstream endobj 637 0 obj << /Length 2044 /Filter /FlateDecode >> stream xZYoD~ϯxv:}ۆ X@H@6]gb`ɿ}LM6 0 ^`S?!P! B"Hw'o 0c|G."x}'1(Tar3yp,Y\|{~ qI`Eȓo/<q$eq R)^OK?~x|wB]DV"Dad$MʤPnT,#(a"@F=e?]+%ex;oD,ڼܪVT\1Y\\#znܤx{UWLr3^dlYe3;|Ul:iоT(,]A$`UNV1M6 Q̘V|QV@]Zw卮e^@="" ,9^T]6Jݼ,m+3薮ְ9K,SZ=3FABcvjXYj\Kꘒ;%x'I"W/Oմ!kR-@nvJ*n,p |k͜+c11ݙL <_)fZ̐ q x_)Oc=̫ig eqsjqC1:h91 c9 .dhLk]u߄D~* c7C0ȹ ~ @bG`]z@ lO.e3=6۠a51BFb `!U3q1s,w!օk+6HA_6+e *!Á[eEvwkgʝ(76  Fp 8H3vrW2`DPİgLvٗ򷓄!>{R7+U.}<oWoڽ}2߾6{";jEa4L"I<^#¾#-%tv ?|=WC<[] Z>z36p){>{6(Bz;ɋO ?\QXlZ6-9q/9;5Vhųh yc1#/:p`O-%Xݙ8P5)]iCUX]߄~lx<\Ցx+(½xUi9|R;,'gNяoCU1!u,@~#8ofV9>sK 'klΜ>RS$ RH9UhX׆5VZf%y2V:K͍KuT]>+̽ nv}U<82 IQTJEu3RNދfw96fnϭCB=dOz ߮#11t\2llm!QiKO|u4y9IBӕŒ]9dg޾=4[,:3E쬹! Tq#1 !<U Nϗ+xl@Pj`7tRmkGk+l]ʵIY<6v+X%xUǕIi9$=*b8lvSS廻.%o)*|#eb" 6$ H0C' Ϗ}z}4пVk@|!.j9aO,%3?JbsN)çL@<'Su-k3#*W$bKh/rx'8;u:s8,FpOU|UKpDAr?  * $ endstream endobj 644 0 obj << /Length 2541 /Filter /FlateDecode >> stream x[[oF~e%4Νd-f yX))PBRq o(q/~09ss3C`ɯg'?xXR]! 8B,bY^-Oi-Y۵*X$m^mKUQ@DtًN>Xʑ ]Կ0bq\^ h(^qxHRK d % %ؐx0(B\F`:2]\'2Ykf^njmnÒӕɳ]lݐ7BMh1pj9%!O?Gm7I{Od1"~iU j k/f&MuU96./"S\{ hGC˩pp0L:֩cSqHuyۢ7ñ&Y&5".=V1Ԫ%K&|G~P~{Ǽj ^RCiVÍ=Gt2Gz CF[`0}J癣s_ܩj[>Kg#'p1c#pښ NOP{y/xσK8nDJoafP@jCQbujj}0SQ]8` ="K"T8>U3/|$$&bkv8&t0DA #ag qʥ.q] A+X c3T"x0%gkvU) bWp^ q>-?"#cxY C ǚ @ p'k+fD,p?@Jg(̈́4Na;ɨe/S؅1P}*I08 `iJX$H6x(9>sV>v!qnyB8X{m1j~4pEP"T+mɎQ0][#0u&2F2˚1 Bf{sh~jVwע3䡵f3NC'9zZuis֎EoW9d}i-zUi}oB:|J.[Z'(k97kX/ĭl4!2{ؓ9d[ ȷȵ|cMp]nJސh݇(S{QbR1uosܯwg`rx_ G`JVÃevE!` _Sz SXQ܁]m )iRT{wzZoGokuq)my<ݟž@I$"g%Ñ AG$fJvw&$m!oIkB(W\3u!w> stream xZܶ~O{mC-iAdnWwZ+mc |g8C걺=ߝ}8I$G&_xb#$Y*LjWrtf7F/ jYTEE,ϯ/  "ʒ`wx } uH ^B2rJr<%Y)!$6o;SM~0NCaF&v̦9"yi6ezk*a= ]n4a?)sPn&4;ۗm0Ўx##9Mݴyj82[DE`w23k@bSwV _ǼEF"؂پmi*1ضbozeH:mPo τ(I,H::GZjyYa(aiii)?r.RAowmLa`C kVҥd&Lb(~ {,ɼ}W9$s{l[ۡڲmiXTOsHvdI/zj? Ϳ7Z~Q\Fy݀bPYxƩBw}5(Ց2]Hƣu³+PJ0OM4"-o;I5ui \U:Q 1q:(6v/ir2 :iYߐ=Fřgy?Ӵk=$SL,Liq*K%@[BFE{NBcYqj޲ΫArc=N'.8k ( }\)rr?6>V ))t}N>JEN%eO @G蚬^uEcS@yO6WݞYbo+: `og'D H .QZD#k^\nKd>-V؀ZQ9F2Bj"Z T$}S,(l;?u^zrӾQGQk(_~:ʦqoϚ i?\\LR-o6WC ,|IfO!D'(血sym$GRu2eb![1,a]}Sk)TBl7Dz8Xzs5ķ~sN:L^omy*0yog Q[wPoH$lB*0ƯЄܣ A&_'F(l2eqMߴ9GV-rOhCZv^?@YJoV 1 LCRƱ/0@s_bQ'LA!2Ը |trA1P +Yq\ykDZ^ނfX2',:z02%^̽_+2Ϯ=k-oXNZ^ Z#4$e;%g;x ;xW|d^U񇧰/OF( lbxm&0He}ISSoKGls RZell&' y$~ >wmAz rN h@k_8,g='|zeڬ@ϚS*y6VT>hށxVgj*0 ᅜ'|wIM *#bu~#UWs"wf{YOV|[#RtZګ4 -TTcW 1FJ5Oe- -+BKƍD7Bd t7ߎu't pQm!7\7D\"j %1G֯C>`uh[뚞z,f[/PϹ_HY<~wH{NhROʲm~h%仜x$ ]48uZ$" F9 J&Lx$F10}]Q u2,X~&):,iSÅP z!_Mz=Op6Z0v+=J6s&{:\+/[;=(:'@+,i9# 3ݤ |cj7Nߴjm^&3I`YWvPY~{ϝ AS3eIyK ^ֻjӾw Oϫc734wgWW]SF+p Sxrd6ա/w2'4b)As` XȡAE{|S0SҲŽ}#ov@M^y|W ''7[M~bӏz Z:&w<' ܃rB f9jՏr(\+tBXh&}CIx[@\Bwl3NTPc0@%*zSbPfS/K++ ul4'Vv*&)qE]y-9(kgYr7^a\ZFK7.DzPXa[6-hݖ{~GJK؟2G3ѩCzȓ\?s]bk?*mKAC endstream endobj 665 0 obj << /Length 2622 /Filter /FlateDecode >> stream xZ[s۸~`3}Ar'Ld&OݬxhP$s&Ѳd~8H[zo/~|#Hq]{A *>,ǟ+nvqK^:np_z⧫.B=fJGl/>Jy(^[O@A9~:.[$ (Cn%EGӤ,ڬb][[jsVbapOKe?[WߢܝԌDg{y9BmKjU'6hDчdH.$ #B >j^2 $̯L|;Qx+]$r*0BDܛ^ՀNSextɼB V:0Qcb )@azV]'rm02VW??wMkKqakr֒ƍ뛬νߔ.g u{,~)OcF׎sVMONv$ҲɆ8ߺuVdQkY.u\1 nfؖi%+Z]q~de5ص]OT?ԃꄑWYĿ3]N Nlӭ.t4Uo0[)=G]̓$ `XII/DQ:2B10;^)0<]"͊[[Ű8>5:52_勩hٹ`-JWh7n̗M )}KK͵nvy;5q?,vW!_@FfwN7骙Nx$u\m0U p,qFE$J dR ͊.M]n7t;խt z2Bң[M[e I_1(_)nV.L1#VbK/.@&'ynMYܫSVڅel$@3'N- 6N'I_d`Ұ "3FH}D#J~Ü}f}%f[@%%t >d 43ºNծ*U84PBNUS'/`Kd~)(X0 nO D 䰁ࡅWAӍm?fI˘c}dEr}'-%VӦ/ Cy_T1Nf<>w{Ţ_;_.zpi洉#2-_-ԜFlF6ݙ#g!*Vߙ3 =LÊ]{[=:#l!E?9|>*z. y`eR7Bƒ1!\Ŧ,tkȀʍ9v(ܖtUyGtوuDTu|{""F{uupj rO\+{'/PREjqۺ`qF!dža0v6ͮ~@X؋?!?5uR3==ω7+hOkhhXg71(K,*\/Y5Ưt@f$\\uc[uY#MQYnMjGMȅ {IVh6s^Hu'S ){ yxWdkƣx5j.]za/jTֳ}ԜvPv&"^s; }cH~;tqB9]d#e v8 'AA-mn+fD5SfX77z.C> stream x[YsF~ׯ2FsHNYW*8bA $b Z\ :AJ)Gs1{:=:q/B;=` QXȼөac2YӢbYY<3<(?=z}zG¨(чO؛B[#ޕn5 Gؒ$ "PGCGL&>q1'q <:KMҩeEMmmljLĨNiV\t~>xTt#ÁoI"wy M#$ w1ţ/cGiq\5Oc 6<t Bae ,KG,pH>f˲z^E(^/1d]\tr :KF&\c<ӥ)¢,6벣% )^%a&y ڮ'0? kG9R9A0Gi fȪ$eQY˶}ˮ՝insYq,jm~HBDOޮO('\XĐ '2RnӃǦY=f<#+m1)_bf]U  "JpBQk s6U r7cS| ƮM-8<~`EBJ4Bm`$ /`_ #0W -J$ [cak(`)VnޤSX՘A:5 #^F]G:*lK핤dWz~of柞S&ܶP$mNͻm;Pћ!1l${ "'WY=̛5Sai;"{oZm~`)ujk3*S[lhp/ $#z~ݷBVL ׿[Me ]:dK|a%]Ztz!*E\|b{";N\W6z5/%H798nl[.,I<e2_e. 餅){+TY[9 ,]YP5.m= ,#D L~yo?@(lNijK)mF ¬5ZB-yuvAv3Xi$u>YE2956R&rS%2 T;ݿTiDz(lݙo.D kceptjv/TQOTp:Sp0t%z[q1l3spVԳ9H_u\QZC.[Į W&n:N]mD!]j/ԡK XUR)ZA (l3Eaȱ;=W#ix(Q)+er 2E MFYȬu4+YmAXUu,pcհ=^ƫ؁bCN6^MʙDx{e%}ND/a=6UiS/ߟ߿܆ׄ u\.q/_Ul ~Pgsj1*:5EPקI> QD 譗كaKKzmYo=Hp$B]_X(VZE愂MC~>8le/OU"xt^g`<xҲg3y` >nMcԷ jcTPI owg&5˒=N|,ƤV iP`Iĸ eK`)+V{-^(: w*/D\];j_PDFaڿ pX endstream endobj 573 0 obj << /Type /ObjStm /N 100 /First 886 /Length 1634 /Filter /FlateDecode >> stream xYMo7W@q>8$#@>@ Im D(Va;@fm7ZmeQZ%AwwpB._+ĂRu_)_iZBHS-H54nB3GD tT`\p'ly@`-2^/Ti*Pahdţ -Lހ0ܑ O`[BEVn, 8X*+1+ % LNR;(Ѕ ` RlZ*CiX+h*Xwŝ:4@kCS[0TyA wQ8F-kP5 x K: +USo*Fg ZjYP Uo$&Ї32 x4@XЩ<{y\NЩ7!(_:C6Fq^c: k ˾~KP3m0 - $hV-tX|,Ǻ?¯0hfPRVR3@\5 E}P辸-W>"ހM)8;[,/oYX>e@PE` wKGW뷯V7<,_<{Wn|ƒ7˧=X\]?^]]>?y'O嫏 ?s|zFH?.pszZ+GȈPLT`x@UX~~gk~}]d֑dzpo_C$ 5X ˏ>\sGdc&G|q #]n'tU'SJ#9GCiGȎ=G"!*п]pq0mx<S-lFuF,/ڍ>.0fkp:[K3ng^TY-ό2MOer$;>i!Ո?KkL82Smz$q;xLb3u-X)m=kulm31~ `;O>SˍYN5 H4Ŋ" vA QGVeˉ;=6cO 5~Ʉ%(H9ptpJ[E{<7Rv0NMvP?/xzA`-XF 昧NG>SܷinefD׎ U 'eE;YqCe[F_[FM/sѯ>AljTסG/b#a;dtm#3J6H,w"+S=L?c=T!ܢc">=Ʃ> stream xZIs6WE)$3iiS= MTH*i}eɴ+l= h= P1nϐ]M_L<߳xi98*Y6VM>\]\>(B ,CŋM}F-3_{t'Ԡr|E7$r`I2ɳ}[z<[' hS&E|݅{MuT$ea(G zxeW~ +vα`BB1:8[-L&7^w݃T[O zݡ47FBB''C4_j:u Ưx( F|XWMtfzvl]C1$0 Y[O 2fB=IH?vz`,(5m>&f% ϞffX#\aEk :0Kc;tTbB/;z4{0=JVć z&4'U=8ڜ]ע/[r^S= 趯j|˚5Sk@u.Zj9*;Td_QY<= wXN, |Tn|g(g7p@g'; (/6؏xz`D= Ueߣ4Jl aO)?5އ8y\< wP.=|HK.!IBށeU>ۄqHR h+h[l.aUkz`*&Rr 5D6EFŷI5 `$fwr0yLYW"av[pu%(SQVi٫d>o)K6im% U;X֕l}I4(qt:oiΪo}#aqxfm٠x^ `!f^6Q34*Nt\ޭ &5yqe"Dɻ`4 '$!c{קn4+Q QΞB;e4;)5)L%ݭUQzg aVW:Gتؚ3> stream xZko_A@xCnM8*P jIIlw͒'w|,,+ $̝9G\|wuG$MCTHe)b)}}PhUYwj],ID/z{ů^9਀F,KO* $//~T<2G e m2瑒 z[`ͦYٻ"j+ouI ͈< ۆ͐/7MShgB/o:@2&o?7_`yq>3F%>ą;*yc@f$\ Rj Q(i8L.K^]mron<`)A{°^zGQ"a]'@ B䞎f|B$nlM,8$4 vc+7&[P0?oNPcMaoBBo0q-z]ء$hV. |ikrz @?7Or:_і}Nh9i`5>u)rb->goѡ2ԂaP1Z[A[2Tb-A3MLՍ_2Nͪwo/ź;>ݕW5QoDwN]r7~>`a82I2Di f[  EZ܅1?܏x*l=DrAl^f;p3^^?<~>UDƛ2vSx/z;ӈ@2 ~}8L+a!>~ O~9,9<!pO !)2+T?EQ*_hZkW]u1Q^n.VHqig40 G}Ͽ~u=XyOl^K&!7GߏA}UK0[S"$"Շ꟭ϕQ=S'`x qNHOߟ^t/sqT]HK0L*dzd]Bo޾v8'jK X'GqtBxjA&oggHpzz9눣鱗]$L!&I丳nvrloNP |t[UN@(=G9|pif \mՎY|J$1׳:>jIvc &b$,Ki\3:B(XƀhPbE̾ecݠ_-?>;w屋 SQ/=CvٽLX&Mtv;e:mX7E91F5џ'V.HiQ71&j)@*Mϝ^/KaLAL@Ng ҃<8ϤOvt>/pKp>My > y`.JR )NÓ {C}da=2T}V D@FT+{?`*McG34G{L $CmϘ_ͺ2(zsp?_˺(fbca&s?Vvbf"Zf=Pū}(w._wzbOƘ>Q+]XoWד0M-8?D[L2ql:R endstream endobj 691 0 obj << /Length 2064 /Filter /FlateDecode >> stream xZn6}WAv]bhhPI`ZV]m%m\;jͻMHQ9Ù3CjqpR:iݯoNOiM˓\/驘U/v:q]"Ż'IH0H'>`{;jpA %g/'?`%j+ B}4t*["!.#x`J_.r6x FAT_LOcoTLt,V+x(@ARH1v%E?b&rXōr]Tay@U2=:`{X=;ٮLLr7Kmԕkv۫,^yqG,gBާխB'z_-|ػIq!- 媪هnĖD_bc@ niuU.uHkk44fa>T8RPoYET;m+*}DGr{ue r+;| Ӭcbk<;;|TrxN.l(;/{ #)fUVy9Ĕ ;3ҁ8WC"roɃ X+sOW'a"pߦ)(N3Nx&!*SNùŶx84)G (:P`jU>6"Lo&S!&{3 [|O_;Lڱoz%=̓/դJ |XHw]aCAa}raXBgvd[Ua53XŒmt]+` T?tAf_7xv9+]g/Lxj]ƼkEmSnu'g#X5*w* ϳUkwZtyt,_ɫ@W/>U+9]OaE+(ve j:U#Hse>@^`ݣ3s6\ aspGA̱7h&cy;R̘)BJB@dm N(Tgo:XY#s9.6fƺp;_bѸ/bK %LdYÀu S$H5s|7hhߪ[P G7bn3ҵ_2O בZԄ&ƐQ(WsOZCM:*^}l.,0G@KFgQdպ0l v?645L=>;]7rh۞.bpͱ{‘\ o^ v z]:]fOc.uw&iy@]ȦHxy^ք.oVbzϣ,uV顫6/G ·$Op6Aј󾹓3B`ya38@gÐeh2B3I"?24&mzp+;wʝjo0aF"I =i Vw857oJ [Y$L g] YP/P05:,9YBe b :BUxip,jiGPN柝U$8ҏ?3T""HMoy1W W!lMQU5,!hszSƶ0gHFuz䣏NnK+/[bK|iNރfM4 ҳY"r s`fKR'~vwokk0эF7cE}JpSL4`!|{/?o DS,җCm@V endstream endobj 699 0 obj << /Length 2971 /Filter /FlateDecode >> stream x]sݿ'yBAdL3fr>%JdQ A?q%#BjF>Fa#bU&kgzD!~W_fֻ}t>{Y7l)mCZN5 (r %x1\19˃ !8 lLmn8v!aaqvp0͔<#8ZtRyɃ:pB!/ukW1 U|Y6 g iD"jޘ,:`UW9/lgxZ$5Ca:h{c?6(:f!4:".~p{x>37p >pAb|HA"Gbnk*x ^FWc4ܫ\W:0~ZxD;yк^ZEct&sRt!f@bUZy,bPbCfTK @F4nI\Α[n 8UYFd!"t3@(e=2*YMjX߂ щ]|a4!Hv2&K*LeW{ej[/ֶf% R #6zBQc׾RҌ숙D{M~cȣY;V$ WحIx&iGY,Ԥsr4ìg<5QI-g SmPe@1?& (,oKFS<2$dM*, VUUqțXJZ#m{ØX,˝GJH(aJHm^а&6Kr+A&3T%l4 9 ^Sں(6g3.,zڶƬձf0 !û͐3,5担qJ.m3cب6m'u_`AfjGYK5h0.%̕7z`-<[T % F>@WK)(TYEð&Y4LOpOבP=1$!Ru~fV=FZ$g=b)ΛCnQb폝^A&E~BeIqܵ7"+2nzA}{5S{{ZfiCڑ?T$#PTwJo2sWfsV\It<h墬EP1(*.f"uj79=;^i;#2|"C0Nx:?ղar7aΥSL|RcvuljFjں2ѥևbⵄ6@3"lIx泌gu!UEf4Scgt̯ZrKgt*l!]g7ZK%o: E#9d6g 3[\) ňa¢HcRR7dTZlj&1要N,5\6ہǀƾLR> JhtyCv1c@1B||;U@k6۸W>}^:Ja1? )#SϑBw%婹B쾡$bk6z7k.žQ|lh0FCˇ^-/z{,t6/L Qw^Kd":0 {mnJ%e6p̾4-WPZj AuZ0d,pP#tPm\D:vA5ڝJcnc u@a;ԥu>|nKX<: "C{H֐{еWl쏺SRN@U9ab ſ;Nܤm7u(~HטUz/QlJx%UR@4hlʵj޼vF PTFCVzfu.C=MIN 9Q!Nڟw҆w?=44$ sKQ =d7ˡۗMM೻Ih39̺3kx(5cqLxu~*¡܉}@j )lMyNf!x] t-``%dqAcMRSZPedh}_f'?~$s'/tEkU(AmؘƢr&.( WY&t B? pp`;;8aI@iɘxt2[ endstream endobj 708 0 obj << /Length 2757 /Filter /FlateDecode >> stream xْ۸}ʃ8y8^WeU~ɮ=y].1+ZcO>xFުTEf7Hz~z!D>{/^D»]zfyRotVjWizR '>/^~`1U^zKXQ"f6TxuT>~Z@,ɟ9=`^ CoF`=.dJKNYE㵝}֥^\@a'e:KբzƮ/uڧ7V{5E^[ h -X@"!!Ro"]bdDGPR8qs&@9sgpobJBP+2>K]AOp$ZޣcZiEѕ.[FuP3e :lOȟ+m_WǴ3k ͨ.a 6X3'#X!2s>uVg[S4'-i34O+ tZdK0}|_ٓt$yViVGYs'¤:MʸZd7t\}K RzE]r'8ˁWkFA(Wh082]$gg*$裋vGI XhT)gDž7dB :sf/Sc{;Gyd'ZLpР9 6`]\[(Q*t#Da6:?ΪBK|.EkHdԱ9*7 >m>U<Fj= Z~~yu3@>tQVWe?M|Nliҏ|lo&Q*$#  T]D"g=A)BEڭP>Ej FS1Kl:;!%-6rt٢h|H*5bTg\Ld]d9j2`5  קUs~l" \/忊N_283.qOp䱴v\gN|QP>kZ E;XMK #H9{t<Eģyf6(d6i"iʉ`lr;O3Kʪ'" z©{dYكFp_6BX4Bg) hѱn#'ӏz}?2IS E(+Sby\L$ yn8p;(19avM QmX^AlƋC(aݡE.eZkhƮRWWM!|<_ g3JNCCs'qFݳ),*#?*h{ŎR d\M`kx,XW"v qN?4040F4ܡ?kTeo[ uʵg|${6z"?hsϦsڔ^j~J5gv@ɂ 2eɫƂAW`f{,S?ڽ 6y3V&@|[["isYvPbn11.+a^"܎0:lFI>]T>d1TQnv+S< }p=n˹{VOv ȌE"Ui$Tcl o‘CLqL4]E n=dt-cuM&*v.vH.U4v2$(GBz);_G8;g'&,]LYJq$sՙ:m:Am+v>-ؽ =.߾G0kw݈7@ NRE~r~*\ L\S6By@rD#>ز6c_1*."տ3xRC0j{{28|p&Zr,%#o4tMԽm?Q,¯C|?\y{ᦒZ4x~JvADRsǙv-/`+u)?z(tpݞR8ɺٱ]>D4m݃ޒ_=ПH]X~U 8;LZ5 V`L1'j7 rm`bӗ 9U1sPp>e V>t^Fx;B /G=L3 ~>D`N ]O폣Ķ4!i6mJ<#5,H >7ި>a8lnvHx%,(z/[/^@‘<˾ ㏌!po'P2sfO@/mo"g>/|ޭt_ P endstream endobj 713 0 obj << /Length 2507 /Filter /FlateDecode >> stream x]s6ݿ4c! 2L{ͥv2DٜHE. -ɒlNN~], 9q|_" b+,R.E㈈Hia_G\Gzp$IŅmiJmE۳Og  4!&` oJD7f<%#\+(ogQ*QV>ʌ-kQB=ޤ*3x,yZUZ (ՐAeku]d tEL׫tjYʦ}`j5W\EY#Kƈi rȪ7Yu3z, c"azdN(V/\,f&յ\e m,Z$ nlbj ҃m"I$kDP??3nFҬ9eOʢJۚF+z#QK'{E ^bʷduO-rZrɮx{KmGDz-xb~!,w틿8óP"XBqoӕ"+;%l6 F0@3KF-+F.nm ] hX&|0B,SD8qt8J;<"G|SAqMh߯NaTFEȯUb4)s":PnPA<}2}-RjLv3Ƕe)nn]u6 ApM~ T$-pj;vE-;y:d8 զP̌x#vde ޻C-:u|2%.b´ZqBG$z ^nO+ b=ev\Q]Vkle i_@$g:^e wd6Tb4+Lz9Zbn1K&=PGV6oy7+EY;i .riƏf=Kw q8;Asrٶ =̈SˌERaG8{ ^ ͻ[!~ 4^GڪZgSuP榻r]-N_Ё,Ӥ8wL9n$UB'$MCC!ȿV:& AB+,qնm .:5.x݂ԍkl0ToB[x]oL^`@^9q84 ,׎ծ} KPoAw| :{NaLrț2 =@v~}RpG9RƿἜ>n]*G`?](0:8JB:ւl, Nkы\X@vcb~%@Jݸv -_BPD(o^j`^l{翇ԾX%gc(0:=})*cJr?VB`O=ϾkHWdce7AEXf`]NΖx$a%_Ds8ƻadY ~Ln^У2C}/',AC$< endstream endobj 720 0 obj << /Length 2919 /Filter /FlateDecode >> stream x[ݏ8S4dI(z v{v] ęit:,Ip}[DIܻˋgʋYwB HzK0Z.3=I,R4O*O|{ӅU'b:ß[B[3GލxϽ_p%r>?$?r_y L7 m. xg|:RN"nSP$z$h?rUV\UX0UCz*d?bwCHSL! }/,i107VcsJTO `PV ~J SA,Qn16$Si*Hr&SjVtnLsXMLٱ2J(>+*}~m^+S~n%jCJۖ;ʐ#JJ=J0(S#gD-tYeR]oKt`i`m9 ^kN =IYhmJB_DٹZ9{>r^2xcF@;钝AJ\ bR5DTЈ-]< ۮr$UVbwbo]{>{l`z"BɃ1 8 4OR e6i3V }S-:-H ;$5]hFެzxK-!a4o7-$\y{`@FkkgXĶI`> " ¤tšy $4|--HeA`BҰsO}^ϻV3Xf2Q@Tg&h 3煋bu/pq`Ye*;  |0Pw&`$aؓoʷaȊsy~o'`g/d<õ1{ZTHD[Qt5,׀nU_1wf+YmlqǪA1CҞ~/OP}J5^9ɑUtp;q#t݋&r݂z)(uMmʌf٭U*H^뫫~Ljb(ХSiִ똚y첢VBaZj ?Q}%dӵVFeTeM|kmQntOzyH!6v~|],umtKzdu)l ՈգwWbFoJp|CçdMY/ƚe5qf~Ϩce(lk[z=;f}g!~SON(?dճ1ꮡr bV^;gqj6.\\zf/օ*Mʝ"A{zPYPf6ӻVO#5n҈@mf51z(q pSl<@XDt.\! xwÐ~X T I$!$2 tB W0$tC%I I~H-l1$1~lo_W2Yaعq&9ܝP Yt( Cu^p˱gQعIpьrUHh~ߣdxUH]d ,gH@{eERw2{Wv; a|Vv I6<6/E]q(bܺawŽݥ91PB!mঃlWZ~YQ˶c=ꤠv s~~u[O9R"- ?"hU ;6ĮsyBmsyYvpBH'A?܌e!ckƠECo4Xh%~h3 8' JI㜍k {ncPYPtAT oJT}*thӂ&iaͼå"L%);,4(lٿXZ!iY1#Ŀz""|3%nS'szR=$iy>zv&"meN`pɫ/-bD s!ޢY%O $ƞ}?%fPZXz -?] X~= `}p>F,bz,^:!-*|zk%quU{P>FP rN_o I`O^eU%$xR endstream endobj 726 0 obj << /Length 2204 /Filter /FlateDecode >> stream xZr8+^N@{5S3*ϪNCH}<3ӎh/LJ+ xxݿ"I*k `^̻LOjMJv۬lWkm^KVdqE~<@W#^@*G" d{KB{jqA $.gϰ@Vvi+m6A+QoM-Ygq!K uȐ2 $@3zfeWYoq?]M~bE~7EWծLR_ӼΒ"t}^֎,E0i\8[W~[d qXj`6KU3W+֡PwvM^n` 0"aV;kbFP''(> rDO&~RmҢqwLwk}fW wPcUU*]f;J.zʹu?>ֈ1n2t|vK,|bd>8ܐF0ؕ|MkKu0+@w@c3gN"R0CoĂzx™vwUJӦpߛ'oGD@2&&p 4;C^ $ޏ`R0$8 āUv_zF̼- !x8n$[ kk!B0Be8I$ S}b`*2(\*n.'U{M r.: .JE*"j,2WS^|U1l+ވy齡8s+oҪQ`?W8 Da$TEn%&vSGZ8?f\ w:LA8 )N}oB8D&zΛӤv0Lzy9VN<BIve %KAɑ$Qm޴y.kYfvCbm3J][:{n Kͪ4V=7MF&-XoR̷kkx܎9޴7hAs =k؞O&:Me[϶"73=eVz?Jئw\u.g#)wӴ=灮x;,:4~pO@J6_<F8LOqdhs=nTh)x!xٛBmP&Pqm?ȻٜU*.8\S ~([ endstream endobj 740 0 obj << /Length 3065 /Filter /FlateDecode >> stream x[[sܶ~ׯ`2}؝@q34Zm8gRi +:/2lH7f1x80`lal?nܗ8b* DJ'XL5LjK崺݉P~ٚ*z; YW07/~Phhɂݧ#Q5( kK߮Z7khzE BwБSv ͎Fjz*jb>mk&hu "5p[`U7T/̩.cRsl32܌WSu^E7Ċ[j)B5G0CE F鄴}usbPv#G:JO곕JX*F?[ Oqۘ =rl͖ 5cw֪ X]sEOs(Zی(ѤYc9:w}H[0oj l!B7}Mo_a$q$LSl_K=J^')H΢00,Ao L g2) (?=P C0JFrôC]ovGi3x>טtB% -  H x@ 48k 5iq8̹}ATFŏf7å 47̷1˒ؘOwX2A&5oo:{4@{ .t 9x&eKt?~1L($8BwRV1ECDK2C‡_3ƾC~(pyq(yxӴ/!>o*v[~|4ҶxNh|FzSA[@KX19EcD0ș:Ճi9Efh tC[ ˴Mf[Pnx4&{$mY}9 i6CfdBo OOmlq`mY/Cr͊ בHba4 %>#qK41e5iYe6QJgLr$|9Vx#E͉&dMݧyI1:֌Rqto(J:yMl("m9*h(nHl6fH^޻]DԁPCnD<Fԅޝ.Bp|]sr2 ړ]ȯjK'˘ GS ^l:s@joF_B`LRS;="ƃȴӥ-ڡjԽnL߸qFC9 u{w,36xT8< N ):ˠ!O{~5;*k$ajk KkBΦ]VEA>e:e+sǛloT┅?J.̀b"zB5K1B '`՛wxbToL5Gwl!x_}N I.lsvl];`#䂊vY(ovN(W# tP }ye $[{9*k#?wm$M'7*6-!kػI>Vk)ÑZVk,hP\JBC ^Hp{@6@~QSsΐ꧟@ia|MM`"i n #(:C\őToOnrglsIg(RRgjp}{,~fhEŢmjq*b4a},R )avdsCǝhJ-1xUQ_Xr4 Td)Leto2T $MR;-GJ3)9@ ?E{JЪ\|R Fˆ3_Ot` Ő셅 $%T܋̭˔7r4vxa5QT{|^bJziTINL2?C<!i@Y/bЂh|>6Efye~({$OJP B?y4P~H1in|aez8 0>+#s#(V"drۥ5:A|vzU5]gvsj*G endstream endobj 747 0 obj << /Length 2889 /Filter /FlateDecode >> stream x[mo6_ ] K")JA.@ڦEѦ$0dIk뢕+m!)JjK4| wNf܋Iw`$̓qDXļ{7{@Ftʫv-ꩦtMˢ(*oګب:|jVimiU7Hd8HB숬 LʭM׊ӫ̧X{3Ɣua@wMgF8jTYә'Ӵİ j =f1N|Xz[zSSC:4PU%üir-IGH_"ץdrauR5f)P+5ʊ\/#&G-˼6էe}~,|$07^W)D ԔDf~°g4[lϮP_5WŪn/rNL& ㌴t\n4Yd7M>Ω?t;ݾVmd\KnZź.xHlNq 0燶݀ؗ^gZ7 [9}H!1#`iHucd3]yLN2P%8tX,joaD(A421-)>Gٍh)~ߟߏھ-?pU I@ 0B0fJ:V-F3$E03 aBTYKcHn=7 :LġսUP:W>C*w`f_~0>AdQʐcmG$4u۝+ 4v1p} !W{Cdy>nWuv:FFE$Cc&Ohx-? XIĽevnWSۇZ}qL]-!Ŝ ad݁dK~6%$-bAYAz9hzX~P *\-E۾6t=Kn!6yɴ^ ِK/ٿj +P><e N !Jmt<r1nfI`&xtI 4Ju"$13S9V\TMzbO ~׿~zu˓_&S9(]6D{ܻ( 8x2 ܟ0EEm*Y/NNu1revxۂjwP{~rDPy(#&f5jLM&lCYrϝMSv4=qʌ'NwT0qx;#{Um>iTji"XSk|F}?d]D._sQd{2-mIisMr@X5k\egE˃8}4]\7s3"3)q0尫hjsS,|G,PLos2uKx,tl87 d|pS_m^c bQέ9fTFȔ^a.?d[蔔,faJ6H`DroՙMmMkeE]iX gaks5|xfn\LS\9> stream xZr6S0f, $nfl\mSw"thHǑ DIL3ɘ$A8pty٫Pe(n$,E4*8{8IgoMQ}zCQyW z3UY-7g G,լM8Aq"ྊ~> *O%V"J8ʼn&SL"~,b1 59ų*?c4 JSWL9w)╦ \ rJ!+;dޅOXTBhX aiIʴʼ=r*fmS$Βݗ ƊвGNq-ۄJ@% (HEg! (#uBx=_PƇr}Jͻ|5n`laVmb?96RRR%dvY~nVԻ w}AdQ$pNO}Y,5~3g5ӲʅCeR֩a(-PZ`G?{P% 8'q課]T?I9i)8ʫ儴hةac3n|5Otzhٽ2 G<7yvn>(߶.m{kv=6hڮ<@ W[;%@_Pʢa_}.x430no7*PNya"zrʀx ++2g}*3N @-{_/_NPȊEPkp,rd3!^-xkm,0um"E}cΘԙdųJ(.P箨WcOwQ}@*Fa.1SW5QQ8݀B\|BnIZ$F{$_@x/wZ`576€n-z/Ʉ <`.Rh4C`qe &5W]58h( H(꒰*G-{=z> >)AJJ C%~-ҽW2R~].eX^>)SQn{Wxq_s.W`+WNv^1 ؀h5zA>=% ]53)b\Mp4z+q*vUۺب6Ԭ5Ye JH 5HSSX|T_O@eM\d&HIcZwZI2j SpW9#8.`z֋Icؗݱћ4Poj3[MoK0T퐓ƌ#"CRTD0H$8=Suj =nKHɂdNRK}p3 p8bc tYZbfvpw1;>Ԉt" KC8G N)ӊO4: cer)͍߬G·2Y &ambz|5܄Uto!9¶E~GgUhk̮ ռ{mNGZw.>=UNPOh\q7AF uUxN>yc"imspuMP1چFi3GOCeNJ_V` -*;'boR振@eIcWܹ0fѡ# ɐofu2G*PF pBjT)^’PNLp9n]ɠ%Hn%f-!ma[|YTM0X'|j-ߺXy޽G=_1BQ ";ttR~lCгrs@ Jg䡍:0m/'rjNLrY;g>9C8?(KT*7.̿]wsMOҙAlvY5]g5Bj9 endstream endobj 766 0 obj << /Length 739 /Filter /FlateDecode >> stream xڥUR0+]QX^r *n!p\x Zc N߀C*F8IDSR4#%Fey5$V4yM~桲%d s*)B9s/e4N208a4[ >l#{JJDB+V,htq5dᄋ|:+CAȊ4lǢ6/I(WK:s !4^rnv#nDdcnl,]{X\gdfcz\ۓkn  En̡Hv9͕mvصM`]Oqx;1-j+ݖ8v.͂ZQRM _nbP,"~֨j@'LT7x8Acz@$g ϳsG|3TBK ,-gw()鶚𶣮jPw$'0dc)V1<,3=sQ<4+8ळ&1@6n7ʪ7ܟΠ6"r%0p?͏i, endstream endobj 770 0 obj << /Length 218 /Filter /FlateDecode >> stream xڕ=O0wm{-J3U|HI$qc: 0X{uHx JF9H | t-oVkѐz 8n~U}\B$veޘ*W$jFmwEoA>UHޕɟ$/?a RTY<%m5jޢ^=w^)ƙ78MIk>G-ߦ9O endstream endobj 848 0 obj << /Length 1497 /Filter /FlateDecode >> stream xZ[s8~1Y=nInv&` ! }c| Z Z |wx{e `۵ED>PF[ߺ|y^V- ղFNr,ì%rwDrc/,)y'YFjoOޮ@ 01ŵq5uHEIZ69XSThEm;ϰn awqs ( ܢ +~6CEd?HD d6A sAc/*&蘛RF*D/iLcW81ŻNs*H kKGڦIu>Gg-8JD.wwf5*,&4#*zmq}(HK 0Byލy v]l炍 %韊Up/j;^'*On9Znh c rCfDT&5t3iNIG뗃$'V;&3'ৄ \Gle;C6=dI&'h \1d]z*,"b1aHd -R^2Ě65kCj0^aplsLI< \ Y -dqlQDmw7 %/Rhk)`^Z-Y|" ר%[ endstream endobj 674 0 obj << /Type /ObjStm /N 100 /First 884 /Length 2216 /Filter /FlateDecode >> stream xZKo7ϯqX*F%Tr\٭|bW*-eZ>Y~osX~o?M?~{wa ӭ]׷B-@ށHx˷*$O? &X|7jno 6{i"漃sF8H$Ój2G˔46C+bxRɎ^sY!,WD b :D^iWLrJ q 2QoN6~TГ;v{ ˯nnnj*-B,V*lI2/>~._z}7Y~+.BwX,H^K.H.d=w 7ZH﫴mvO;<틀" g(vJ^DAH.(ᅨr$ X }]ɐHuWG~3Ď8Rx%3*ee!F*3傲3e<\HD(#Gm}#G"EyA8kޑPw]OsvHB@L\2JkCoFWT8f)P%GDֈ.t؈:]Y=)Ё'෷ ؗh~OsمiZ%c]ʧ8G'Ψ=#n\m&QO?TѿY u9էi}=Ԍ[PH:?74ه "dPxz-6Fj:?Z1)fl+qvbfE-J*0O:L Skg8 ?!Q2I,R8?lnت}:JY kqzp(Q}CE-4G<Ӟ'[m'NrZʞQh1G寈p7GM43}D0CH4h?5#:]\boccj06?qfЁhOqyd(EKF@уЈNJ^鰉H69nw R᯵bP} 1ҴlzzT릶ϺjPmLirAj傆8C)!qsX@&oEM(Hle)ow;keeIquԬqXV&˝_asB_eJtI]sFE݆xTGVPVyB3*|I)X {B 4!;Sk@( #D3͍8}]gzDIYfcwClx~Fd/g:oWj:BM6 ?|?|oYذ gB6΄l Y漭Σ2F#Qƨc1?hN ~4m24?xo΃~}|#2`}>^}ׇ];{CSE=(!ά]2%[@έt~С8n}K)Eo<B.( 3p|3+ QM Qeh9 qIuTTe4.Dql`υKFYj [GM#0tI :g2ȃ vn endstream endobj 899 0 obj << /Length 1099 /Filter /FlateDecode >> stream xՙKoF{hDZAӠF{ DUJoTE9b[4$hz=y9+bBH3p4_ۗfL/I[M~5 .Ȝzj>gB A?U`i5 ד-5"[Z#!)fZ1z3sB*F)#ӞrK7wyRc N} (Go^iN&5|ahrTJYA\B9fL^iE'6@]uiĞ~+#1ITp'?q8Li 2[f\Fe0yiИhLl9}[2GilAC++^* xdcIKeSa*ُ2xzJ焾GY<ݹs{|A| [9*w24zc]6VWdFΟOi #7Zڧ.@A|tQ(lq[0;fwӄH+jcwr~d0l; -ƚGqlC+8۠mT8Y>X'r 'Ũ7>vGv~W'o{sp hg>$usGMI߻up{ٳzid׹CvnUm~ Wm; 6*> Q#lR#5ߏN0wE!싸P*1Qfhi?C{opuw'3l|)yaKobdopm endstream endobj 852 0 obj << /Type /ObjStm /N 100 /First 915 /Length 2613 /Filter /FlateDecode >> stream xڽ[K\ ϯвh$")p[ċsQ ߏg43š,40|Μ7EyI%TqDrqԘpmIUpߺjs|S-7=`2RmxlS5.]RkZ4kJe#Nb'bloAK6KMm-7-1XE7YÍ'{bg?x-x[ŷ ╓08FǚD9hXcIS+dWj8Ԅ(@h &9ϭѤ,vR 'O-*vv@QkX9Cp, U5-y%L 0M9]gDoƾJdw<H ڷERv:mw$^jhC984Ba*i94U$ĩ YbP B@&)j+JGBx^6mni&4UTBYb wzdE*]N=<{v8oO}/={zaǿ]>נ )fYaZxL`87źoӳgc:S:>Oʟ7o`   JrJ\%$&A !2D Rt= T&CjHQ^wP-+oVs~N@lFW"hoX j lp:,f&ʢjn&wY@C+K0UN MB3 d;B~fm!X< f0 dN(,jhBܬms2 'B 2#W"4ڋ"ס]"0WL4BZ}Ƈz͊YI&.OB6xA&f ׈)\$fY'/ [:i6\t"2~{QhTE/1@0FVYO i%_$dji=I pըj.h뵲fgPÇ ,-Y(#&OR P'% gM7)B+%EF*X!-H#dK'lX WpquBdP<5$JM! Pi[00}0,#j"h-;a8$  IhWR\'h+A b@^G71,e"}SHUQ4")]1@3kiG .mj0 $vEKFl[ͥn[ "W ȧj Jc(eT,rdEml2#m8c&B(YbɴVE/ZzߤhS˾OO"`@qrU#>D+ea!'BD ,(ȕӍ#:n38vIk(0n GB|(8lh$] H&WBp8H-G/"YaG/QT^y˩91G8 x^;KI?OWǰl_}u4/ֽxzI,߾"&NZFsPL#m@ӻt|K;cۇǏC)`чO=#;=Ͽ1ATѬy=Hg=-I@UƵmAY{lrql }a؛*]58p[Xiv-?u}w *sL+k)"K U9~'@QƠMhN.P89Dp bp&:1T YC1嘜CI΄ 38nh+oq>Hnb 11|T04h+HQW ;ubc8PM Km,_a#l12;Y_:1MSCcRD]ȥ=BWqXn˧-őunT}gbWw:;Dˏ7}iѠ+\FsZ+U[QEߑnS3 ZhO:>+aec$rUZh'A<"VJYaH#a@IφBMla([@p{1L/~3aK;HChS+8N oBh #O5x1Og?^' endstream endobj 919 0 obj << /Length1 1396 /Length2 5927 /Length3 0 /Length 6885 /Filter /FlateDecode >> stream xڍTTk;UH 20 14ҝ4ҒHJt 1߻ֽk{?{{y>V&}#^0 !x% ( C.?nVS'o%)(rD%$@ (/ C :|M8 Iw::!Pa Ppx@A0qEhr?JpH;!n>>>| WO>,'pB+dowu0GSC"x ' G僼AP s@UB gJf8=~YXʂ8.0?@e9 'K? Q>B>)C?\M5}>Ju[@{]Ew䋥 ~缌ێvӷitӤhf =hqd=Fu]i>lT4#PS\Nt!ŮxC5{פxLlf-w>d+} qO4C7G"U锅qM HJةݕT,sf tOOܻ_4Qwk>OHNb5][+0^646~%,ExEǤWy+%ĦP=A$geBEՅgc3#c' ;[<+&\dȤ & >-E~zAqE*pLJ)Po5+`q)GqBZ\t4JW,l vxS \ᕄ!fpFdؘ,#- O+j Y=]-] aS$䋉Ѳ\OCҡSKuz/@5-)y[TXld#l=TFj0NOR E[P>};Px@-tERֻV5&o`t+戸д)4+*푬H;l9,ixbNb$ 'љImak'ss%RgkH?ʫbYq"=o)k\pj,*Оd#~%䇥MJi͎S=|gDecfFQ>΄1O sW0xO+vۭhsk|%;gTȵQ8jѷєR~7+!cs10''1''VMi뼫 W/bh>PqMKG06WPVƴӕ" /ݥZ,}b>#R~-S2c7زvagL!o.vl‰4ЯwmQD^1b4-uK_4pڝO3ݱr)5a DYJvS6EPIt?BsEUbP8uή6yx:o𢻤avd˪d!s[rR5_Qql6{&>A3o|y)rl!lVU>xaY;/Q %Ξ<;, {ȓ4r@ y~Y}r=¾Һ9ƣ3|6-Mxk-u -ŏ.1քQ7 hOZKu5esBF1" kQ6-TϻzWDi85L2؉Q,&1jxhgk@f/p͂&=rRdpBXw F“ ],N)gw3ؑvt ߆<IJov[dc{o\F14z6<ݴm|bT|{.P]< &,ӻEߠn+9{fmx!j2zKnyԨ.'7Oq7,nF4D=4r(%KՃ T7;b@ח3YZoq)v/); E7Ibz u\.jEJbhv 0yLx-p8=}/֙*QNഭrN)0n2wOV6 Sh.\w<}RF,6w6^;C $I z,- ;/3M4OD{'j@߆3J"n= 샮d= o |O-EW2LVC)ձŵbؑkl 'bB/ykk- 1&iQd#,ܓZ&uXjygFs;MT<b^vkNɮ>ݔy N?lӞep3V k{AFJu0MXRn2i}rEGnE$9ƥEHa{dwf$aAsӕgv*jǑ7Heh64з7Fg #IaL~Ow4N?@pU? vv9?\f S`y4,Qz" \Mfh~mZC,(7HϮ| N ʊR(T ?C/-b]̡CT. sV2sMeWݳxSդ)^mo 3CU{,Ij=tѮgtP~H-uBc79/g'L;sol(B>2`xI|:#L30NYiueEyf[}1wX9<49XŨZp49sתsZ>ǿǦH ~ޖtf܎O},vD2lqg!֫s3(RqǔC:^,wKCpf 6Fr_ZW1ff&g(HogxUp;/ L_.̑7ehsRy 2C#܎Xtfǖ)k'.#ñFoeߦpQ&Ei`uXfi!$}M/ўV~{v8_~PfZ꩸(tzU^lڶG<_dflPC6\c5qPv5kF>M ³ۮHoœF}AZL$Cc ͱ6J/v)S"H/ ޻3\h\|ÀtE:FB,.'4f빖 LIR5T ~aswKVREM':?#ach'>sI< ދcplB$EoѪ$ӠJhKOJkppn>LI^}dת-*y[zgw>XUI%|L@!`KX4)"@<)\۰]rrM1_.Ml]Ww1LtGg~̍(]|W-lPᙴ4 <;n_܏~\vTo0Kf@TlU(˼l+ S"|CC~'/`D<*O3ilm[,Qu {2 .rJ_xK3䷖޽s@Zx!|lSznI4;Osܡ5'ו56 Qֿ4zvjxޓ)M;2\/Ob#"M˯Ͳ# 52SXZN\гbkz!N.7:alˬZ~[,]A8cÇ|_̴>O?VBƉ6wT&g`&(5ŀ/TI B747 tila%gfg9kSo;cvdeH˩gdžr\6)DHP,ZM;ք= pd 2G@Q70QǦ5a۳"[<ʮCzutpqWZ^hWBSkKJ`><4tާbpc։|¼Bqf i5 D"*DHbgC#["/Xj~J ('RT}*GWٵB3|Ku]L^3sjTOhJ .ui~*{Hf$n:^~HDTy"ӧE/)m$\mX<$JT<.XF K7Z o ^^Vҳe_f^4FG 0E}2삗 ɍٯy1vDnx#ŘOh\IONy׶) \.;0ico.ܴl׭m[>12'fZ;dԟ-.{9rsTrV => K|a؁I3g=ϛww X>*^6P^9_ň _ό!(alAnqy&MZS0o}Va dPSR, R.1k<+CgbUH tPn` yڊL+2Kfqj `{"xQ[x|ߧ=ق1=@䢆H4$!Ar͟ҁI*޷8 4qB& T-؇PHDnZ92|(14PD)G:fAB BN=٠ph}yiLE]bWlr ڭg endstream endobj 921 0 obj << /Length1 1376 /Length2 5980 /Length3 0 /Length 6926 /Filter /FlateDecode >> stream xڍvTT6Hw$CAf!fE%DBZAJ:T@i~Z߷f3sЁ 4 # HPc1C ¤H⯙D@ `mJ0 FN $.-$! Կh7i@ 扄`@Br+]|ܐv6xm!)) p@ᆴM@6H)xe1iAA///0v{@pD_ Z0gğ܀=xAܱ(8 n@.?l!п A;P>H`tB*o ࿀0'w46 C:ߕy]mo{6nH;E_i+(;n# E඿{>@!]=jJ!Xlv ^Wz}o/3? `mE`HaߎH8X#(dǚ!S{BwXz('WPHCzO)(?a!@@JT$fс!VO H){J*yjg.-4?7AloW 'n sF:`9_U꿡?DU:Ga, $ #Ucc1~) Bݑ-(|Xy8bXZv!(4̄̇e'#Ȁ `Cl-ڍXEDAlϰ>ps7 k[7†4vC]xI<0r{Tqog&>Q긫4ǦYxue,(( <{sqfHZFeeoz3p"B[djv (:He\cS͑pLht L6̵|{=Vo)l}`!͓ӡpuWg*Cz]Vڟ*wLz# eqsNH؍<zj)ۦMu&i6$+Kf@gLu- kSwg+︬-$=7lHL*ϗ:.y:#WzY-y.tH*`k/Z)4tijJ~Jffۦ>*'6ڦ>CI@o? UW,+ 4||$e\0="p']u#+~ѐj} ;aLc}0R<Jo{}E@'$ ۙqө^JCj`=߾bJ|a}IUoɡG_#Cp\F*ٮQݞC2_jM]nf&1zF} f0Q<R?*VMr _XnXT%E?&CnԚܫV ݯJ,kz;\+3ҁ&q䄹cG˩cS]3o|G&*vءɢ-! (8zD.R/Po%KsT>? fޱnVڏF^ȅ[ѯ GYI߯'QQp2< A˦OEl|sbe =ҡNLmI`еqIHPT3<'=sթ\1{տ [5|g"Y@š:{l[ؿƥ!"}İp|[RĪN%LI6DxBh-*2bQӍ~i*xepL(fE/PoxH|6e{!{"z*=;!vC1PZUOז޾Mjr\3cym/!%Kq ZxPHP=x.Q 5y$l\56:6t_hY9nϥ"8rjl>Seh||q2f Yr?T?ϊO*P,7j >f4S i!>嶙=hTQZBH J0*s&awfyhy h~OA-Hw djx~v9\v7]?[njIΊj̑, юYLMD.kǦIMp3fU)浏`%2U4(8Et70Po}wJiWr bQ%?DP-*>R&W)]8gBF,e#XL<|` i>ٚ~%ܡ"ȋ,;>,Yld Kh-T(3aZ6̣6CoJw/ZhHhhs+Kmi۰CF&ԾywʅTwSv$hֈ"bQ.Ǔònae!{y>tRl̮Pz}6iHtI1 $.ˇ鉭Sֹ&⹈{Ծ_B4dĹoW;_-ɜYT 'vQE<)48H?&yJފM\GM zt? <]wѰ?#s[i?be6}27]NC\W*JS>偺j|~zNC֪b&2ʌ BsQ tϛn &D=TK'u]ƹv)% 60uL~}s^Ib>y"\ݶ~4fy y,H=+H#I(yj~|J_}8 k=mξȮ}jc'G2KE[-6ĶWvm~VxiX2k{}4ũZՠ]v²6 tkR8a:w'y#UOpƲ9?+H/AΪ Xy)=σ/7F]CI=49PӴǵ^|ǥ* [n-%=tY*c3 ~%FhNOTغMjRzWHs&3&جo4_Ofv!p­ "WB>_E-sYMsRqTAm^1nn :ѩH6,ґs-ƇNJJ{hAEroŶ:(Q&k/J|9m3wY԰)KO rNde6Dl:gGh4RZ+.( +.\'PQ=kOc_Xk(ޙ{`B&;k&}^rCb9`ϲ9@~+Wd7/E![u*&/DWE }Ï T"{3657Z @k+\r^H &44 Mx?P OԴ6Dt{a޴SĉsjԮ5JJdu-eVk殟wϑMw#V$+![;M6H.tϐ@&'d^ajʣڶΟDɌ{ZR. /wB|@bO4~hJgoi&oHۜ0Q}'~6jQv4rxWav+Zs=92/Ѕ8zW~չ 2vqZIż7^wqH~FJ&%g96^[,]Ea|ڦTj`Qxo5p,sn(:Tl9.'üE#eI% #ٲY( `EF`ًU.$~.Xحs ٴZ20TiCqfn~ZV)#=HܸG>\&y`.Koeƕ+$c|5Jo'0M\.gLRK1t1"GcVKZeMi2g,0ZcӺg__{*_npRhi=kFy H.V2 DOWAFȵ+]53Lu Q{f0fGH^{~+z9cK B> stream x̸uPJ4H, Kw#)!)%)- Hwwt zf}/fT5,A@i;3+?@+ ܘAvfVVN$** W-I`uX]쬬|HTj 0(4}lZ@dn^:Y:& gW[k_>x~9X\za~9gțY؃mfNyf%f2 ,Ђ@3+ - )u 3@dt,l\,܁n/ 8?x˚94m 33YIhhj0$Ŕ5@mF&#@Y, @엹˯jW:dZڸ;xyy1[{3\~дrKd n;8~5hk%wR@E%1e9i) M&p~wݽ.%&$?Vѳu/O:1CC78s]0`G;9+0g7ʃIZEYIQNBJYC7Iֿl?Gl~SVTUU8:g @[-Qr @WjJZrwvj8__FZXk~(&b?=,d)rtvC5\}Xᄋwy9+['_%Xz8h9ٺx$!Y x[ذ {K rX9lH~nft`ikw9'+b0-shdtcX+$e;xNh~kieӝV4su?:uZں[K˹"_'x#z_3natsp^_E+%'𷞔<3WW3$VXsq{,N w =WW~7E"X$ ^`7aHAl?"qX N(A`.Jo A`.hA`.5 ִ7GIXX f=)lpypX/dW/N//Sqls`;]mA L/Wbl`nB@O_nC,+ځqeNߐWm=,m'K \$Ͽ 8OvvpT إ=O߂<8TĿ/?'?]+ck ~djm r߿G?_ o?&.6x48~+?l-Z}3_5Z -΁,ؽo(*c>z-+9A'E) i ΢.)8Rqx^kMT6 T $Df RZ$;W9ՖF9躏c{H#7h[)*akvu^@'L0;!֬Wl~Cy8. [ 3-Ͻ^ /x3 s=vB1Na6p9%邑E\ۋ_ŰQe\Ae9oMĺ+<ã9l"xƚ_*[^jQbj粰Qe|%h3s ג2ߌL*rt?J$TLuM`,S-T] ~$c2 W TEV~8M:vUYtk[>To_rk~/*>K[9#. 1cȓjen~X& >^SvaMgܙNF{)dL뼰OU6)M*Bg qԔXNJf'0a>F8o%SNO)Zj&]&ܖ}q~ɱhc&_Rh擯_`lk NÐݧҮ&/ " ޾Q &Uh(hjt}oO>Rs}B_.65IG#1J2m2y|rD"Xh9 ^9{oFT)lqn-J<璜#<CYz@{(eQK,d[࿮s6ì(+bi2%l"ß9Ifu<ڽ BDK oZ NpOIT-u:u:SW zT5$4vm lrAnLu[0GGzЙid=TYΨS*jgs~y}2FqvmJs>aETRl;kMTі>^jm%NH$sf͌!L&5bvȗouMu4<ѢwYA&Lix< fRjب?qx3R~G;F%T& ϫ ܾzYp#B蠦DQZt-cRqi8Z-Sm+=ŪjC̍s(H.;JѤÛ Ab^ Ã|FM%AuVZWef3]FL;R:;<썕Kʉǀk<{DMEuyp||] /۾HT[EWy߃xf-ǧbqX8"տ g?-j`JPMɿ=~*CKv8h֚W1>n XXGp9aZ_]G?˜+ `@qe뙆:^TZ\6{-Fp7϶ W!nW*E9屶l~nSI6:_pXWy?{a>C<?Q[ ɏ)fK^,CtLCGtȑub6̸g]P\r͎@<0V?}Dԭmns˪G-A{lF pǠYD^t\7׹K9- bqs(N Ko1 3 .P"/4Fkk> sPi1iD_j¡B,KLVZ>rXtcϵ&e ݕI8,<ǿ%A2Ӂv{CJջ=uXAc'qd&|ǦF4Kk,ZzqE_}Dm^ y0nPwjQ1F?4vaX{ ]6%yQg9ٙjE\V4=AW;{vO%lh+e(<qj9Eh#Qr8Vh j,hx`9?nsKqT5[m[QM r2zJ;,߼v^\"~B]AŸ“M"LNUtR䬉lW^Lkhq *دFČ/z/>9u8+\kw 9/焠J_x=/Q}R>s>@v3(Sūw^C-kʶ5s:ɢO.-nA\[_ "wBy04l?Oୃ:|~si:c,}n:H:ߐ~ȩpMav&2nfA㳫xC7^fME@O0ZztDOQK`&ε1gy~,p\:~[0&gZGtP N/6C&kY0m%Uҏcϥ)\5 P)'89K;EcGEu ~bD+ +_EDVafW8B^fwfFa!)XL 7SЛGU-Mpb@R@ \4@Pt.i;̒~}Y&Soc=dl1x}jS)Y{FNyocгd݀ӯT́IO]-k谔WE@|}l>9m[6W0Df['dP 1CKɛ>Ǽ[G poD(PEh0޵{puɾ9\Ȁ rZOUJtN\4>HeX]Cfni4xGԦ^P"–o9FS,št]-/_/U񽢓%AX8`xWLn9*iͶW&R"򴐇Kݮ[zB ZuԸeu'hB>Һ| 7~0-kSiw8ni#'QߪL;?%YSk뽛+ܤE6 cT&ד4b^ܨr}jf6}|\zчMġae<#_괈Bs KaL'(g0}46޳Б#ѨsXaʋ>(۶Y*L'BԪ:t5C(( 8 Ib[6䠧*"2H?kxsZ=cKpvvhOzv9y yNjΡteBgnaVbcfFLB5Fͣ0.ƸHi\46~dh.Lw ? 0*NktŮ |$39vp>`ġihaىNFl/k,_ #E2fBԅ\H*s!ݎkh,}kmVϸF 30p)KK8P*w$}}Z1)5Hq=;j !݉CFpwzUϒQ9-ƾL=[[WT{HV L|q\+H䕹@3oڋWu]_3,η`7A4Y/껼<t@憕'E)<Gx^"EV!;O+;C\gg|Nj[XښlS Lc$9&Hn,Uj _"1uPą\A~ (.,P4kZX>YLՃ:ג09m@U@ $RC/9}|Qx3}gVYQ33Vl*6I65ହ|a2RSd `;6َb%tl!ۖQ4KT][J>Go)*[ɶp"viN!G0& vQ&t*lCe@"4viGY164 b5N8tl1R^qRvV-kS\va}.*&8ӖՅ, qŮ/ Iqj%,F&P!۵h(LJ Dpr^%93J"ߒH.%ͻM >ٍ},B"+r買όmpuɦ:3>Jዾ"qc1Rt+:.&1*'N$;-{J%۰SyS٤VOD]BRa(UK0o>6/;;v4" hU?-Z}|%91Dk"/HWE9&}};C?u4IVz#֙y^VIx݁l>s&i[b %'To,juIP‡!&\NAT%-dBq#cK3f/.(,с+Lsk#:WTiܮQ fry!Ď2 W\{w9 ٜ!Rm.mM}|GQ?dyWx!v+MTuKCJJWȎ ~Si$4LN2u!C ?ES(zƩ`E :@:Ǒ 5{?>uߏu_-¥eNVoْd^Y%sWNǴwpVfo[m8[~ll*9db5}ӻ1nӘ3A^>f؜?AQG+үݭC&AJ*6rZM=fw3^<3u,cTY-h)]xGh ^ > @?0ʆ[U˴knTk>2}$ $r\;Lߧ@JQ8>/q]鍕FPvnìZzqd>Rr Rfd\ );m?fA%?6=PzMFY_eN'Q0l +qNyiy[n-=*8ev1D> ʕoz6EE u)Z"!a\DvnqO3BX<ȃ4 l\aBSDQL@~I"X}TtIjͷ=0MĬ\mp~*8EGHytq+h|@3Fs^-i,7LHfH`ZPp`R+ sQ^L彴ֹi1 St$#כbtFLAUoo#N0kP|tlVKi;#}떥t3G7npsQ|d "AÔ_F퉄dLnA~uǏQ 5fMёaY#"4|Ks試Z@m3-(L~v`و3+OJ-pR4zmO^T< CH<Mݼ YF>Kq) $ܔfSBug9zo@xIw¥Y{`ԕBP̌3nTg3;m|'Çvl `-0]|͔BW[mg$ldBW%85E#~+jBJ|0̔9uv2:yx}-!iya # l'7{&O2z 𸾁0{$T]l,u l'4.Fqm}9Sl=+w-d+yݢT|;")RMPlS)o)vɯ~1tsf4Wn w;YӓM\e;f jvsy(Ep?p}C>Dig|Bdr:qx~=/cޘ᥽V_@=0/nS1N4]" 90Z&U>\"]9> D:Q\:AeZG.?pS.cqs7co\pc8k,ЯHuє snʭ$}qk}j \|J Ojq "n5an :b)q |}-W[UǨ{stq0l @>ɝ8}"WxZA:I~ iaI(G?gP Ns'YeS4{] F;y" E4◎ͪ8osΆgli$OjR5k#x1u5fAo!ϭqnĮ +,ppOl1st{Y3<~xwt?֥*xA (K| 1溪wS4`tQl70&zhݸΙntDze ߾U|HGB; ;]a;P!+Ffd*܎>@ _뗢_F_vkzc$mQ'X0'3 Z[|ˇ;UYm}ء@sEa˥j;cM 푢_Iʓ^zi&lVIhygXWwR݂zlcșjA8/~D."6uM,M{H4 E< mΓHFu|=$`'O6jQi$NHW&< ΀gm\%D#յ9kPl.kC0.q*5響 -O9uBLLV8!Qp푴BV1z x"BA(#dH{p"u"Z<}Ԯ80?:[qsk,pR]7KCNu_Ehƅ@/OZrI;&.mBa5e1{jnZXu*lK'dqbĐ*XǻT9硷1~%y}dUij渹H'o$5 ^teۣ4%j<ގ6sU5 %2wEs4e9E NXyZ34A˻嚹/x<1JOXg>Hv2NVK30Sdrڄl{\u/Ѐ4bpr{y^!I!e mRGć[] ԫg(+rzן*VY.ʯu:(ר6%.rADTfS$L' O=ͅJ#IHG R-^hto-J[ 0ol˄_fq~yɎ=S&qwgЦኌ~ PlK,BՁ[! ęzZyg8oɒ,B+~-̏ۇg_Q589r'e8Th;۷aBB|݄` 3z5Eт#\`J#+}Sx nn3;{ P9ŻBEV.5+.4#啦W|ApP'8R7?3j VBu! 4NS零Vm(Bب$TfSQ" *&СN0O3*[,g >hP Hiԩ9|l}}3 kǹFa[Q嫸W|Dڷxx^|"R1닕➟FRWq {bؠhdx0ۉi,J. cl:E=`6}Ҡa:2+č]Zԇ6IL4߳_M{A ^H/'<)Vd @thF5V´8=ZFwGGm7MˈU~yd?KKz*IoUt= \#:-g[x09^݊=nmL_h\v> 'lBRQGqd>NP/iH5՝Ҁ@'QYB]lAzUg vpJSȡ@Xeόs̐ +pPCh 5}RA| G T@65r9G5?p!B y{:'>05A4 fŴJiJ>թG10psh ap!jg}xGotڕBfn؅6QPf8vJUJ\֐*]SFj+n̐1YstJ.5NkP#oSCE! }pvw OnGN 9]SPˢ=pSȹsk *639Ϸ6 6@8 W/rpfбtJ-o {tadzN6:7f/ +1\Ďc?ir" M)|g[Ayk20fRv Cn]Anҫ׋a5oWҝ8 ,2>Fm= $'CɈq+r+g4(\w"P:veӷK ,'UjEӹ>yV+ΓzG/r<1Szrqj{q()jss1jۼU'Zcj}w@ G $u4bT,s^׉v6lTK0w~}(ȗF,k zVϲ|%1yīc@?B>CozZs~R1fO'4=,8ݼ\Rܥ:b&S'Fb>5.@-$U7[ICKRKHw=a(84W1V{2YVB]p es:I_EMdd?G'nA~5T2d//o>рў+|&j #Vhb\5r[!/D%s<}ldP^jŻלU՛j?hM536:P[&qΰ}:{ ˊf0c+RQUq`!ƕ?~jO} at3c\8(E^8[}[԰5%ÐUBXx.uZE/$k8rAx\\`.<^YdcҔ/Ҙ^C1ZV|C~N ^>߉A&\LĤMo?k ˵Bݩ7KtTO3_g.EjFpdz)~f(@<@~wYSzAy%"9n[{21xCC7́o),On]róp"4Erdd # l$hS!5%/{)#{LҍZJbTfgQDuK_S7 }5iSV( NO,GoAʹv6y_Yf/Lc~ȋ{V qz {;\,Y7jb[XBxg@bIxǝ"ї uU{rH, 3E*:5GRʭ.~ܧ:#h`.f7ٕhm'؏cdr݃5ZxO!eP(]\ֹQ~Dꀖ8^niA'vQ={yz)vH)mLWU/ 5 ^_6n)ڱ}AT4~)xUdHNE)}0R `xlg/9:W^$Xw8 "ҺAk2 lUІO%PJH6YW~e*w8x|ASC ~ yfa3bس1M;LaQ> ڧ`OΣv#$(`K8d$AbTuŒ̼wE+Yg"ţciyKy[2,tubC6%%^dRZ-k=5/"x)Q"^|;}yf>-2q1;6pFDKTԨ6}ú'䗀8nBu-=gފCVA,3aS]o?}ehWhh6ǺQ .I@1IyuIo.r6ܩ; ޡ_mGlܶ O,zTk>nX$*퐬E&xsb}XӾ瞜lMg.oi L6c`&iR7m ґ2\R_ jb.c3H,9E͕4k< 磕fU撷y|_m;AVv§m3Qoe<S4,v D^E$ħ\EHrymGx=/z0n7 ̶~seWٖ'c%h̦jo`w9Ɓ eP)Er1f\r:l9$a%P6B#m$7ggQᩉ1Q"Si$[FE>{R+d7LLGJEstq+2hK2 fP:M3>#~tW8gcKw^AT*]h5#Jt8,h̽g8Z0ѽ:T9T:N5…:Ćit WȰ"_+&6Up\z u[0ӃtKH-Js/3h?-T8̾y~}G $P4{Dn1 kt))QHYsQo VU5JQyPw4.=kTax D-C!>{V[z1'Iki|~IR/fK"41 E1ո4|E~ nbDV#8G-$.ft,%1 3; m:sW-y?ps/,oǦ5yGo"*S:ɖ^(ġ@{Ά{ -uN4Y1[r(4W|ӞYa8#٠chUW""IEܾu4)yrҮPM9s@R֪T'OB>u+3;ػH:#E }Yf)1`ES^ 4N˸1>{05NVAߑOJ]e<< h Bh$c m^  i}]+f @ !Mÿ˜ߏbgę~1ňoYIy ~K k12c8̵/|X鐧~J:n8 L98%'P(AmS>u\7Yt]8Er/[BH's/Jm ю2Cwj O xuv65zHi7DȔ|s+˰:Ǖ1 F ZIZ WPMjYjDx(nKb#x_}C4א/grw>7٪EÀE`.@rIim!S7V_PN ߔ1쬶31&mFV0=Ow#ĿJZn 4Bs u :_J yFjXGh)#1ya$SFd`im"UZhgVgBHܟoJs"@]B}4E6x?p0M_;4pq_tY;sɗ.AjlȘT2̕z8ן9sgP]9Rx_ZcYL:&%Cp:~,Cl/!chWM$Q>ʐ+%&㘑fNܦR?[Z:S_Kd+|.(Y"DZw PSJ'ݶBD Y'rLp[~C"6eSa;NlfJW̬hQn5X!ʝbuS7X2PH\|np!P[Y`vSᕮGRK Zlds{s1G02t (HBa%)D1|+0_o V{ .БO|3I\1+Rx nFyv/*0Eɥ!)ۣy Ďԕ:$NZ; ;*B{J/t#I}ݻMߜ"K:{s"(Z:[:ɬ]*w]p ĿK::OcziJf2 S4G8 gIg2ew!p5 yw$+vm%2{I^B8Ljײ%*tI 憘~HT# v^MYKwhZ &ܑGv:^ UZ=+[dwU>i?W95u9kd~e9b25?YҢm¶S^cTƓ8>GhPVg 4$7ŋKY]vTxv`>a9[+M˸/WKʋgٻ h7Ϛ$09]j0o-LtfAaX (g[\aA'!s?5zù=aaC]xZwZ-޴5|wAUm2{LYS>9wTxcQ͏+3뒸 *Gp^4P~ooޤ^0V [˙moy:.֏mi(.Wpwqae'mfyB*iߧum9]Fb9t3bqh q b)<ѼN$ 5*GB'))ԬDz^*+WQ7 3Jfĭ)MR K+HmI'HQCqkF}iry-ѠFuILѕEՂ,KBQ8 Q~dv.&42;cB*%3 &Kbwl|{hﴼss6ʄ.Wո3~&qJLȒ:7f0KjHKV721Bj :^3=;~S/r2_1rEe*F&0mp+9]~CINp`\&!Mt `G S]T&G]J2p }6uWu?KNSնc? , &Z~Z]ZJE-a(o}{pnTMs|mՍlfWtN Aĉh^?N*+sUWu÷ #vC*LF󤓮fٍ8Y93{X+.:oŠ}'^&F1FzHs@s} -3)2=M[^S؀i%Kʍ[(zm ]-d,}XwsˏJugMC>@)Ѵ2fE$*Vn.z QTBԄ/#ZHIӝ;[#jL=$6Ndt'9`K{#yYwb`%T 4N|yuDZD 8'X ,B6XB)% ^6x:cɄ.,' h.jG8jE%g z;.A+/ȸݸ/a-7 ͢nzpg_z^:CR[]eͲi(Ec(ؚ^KS1j|`X": N!'%NMa^{A?KY}-(؂yś[BgD$/j];2pv-h "~}[|{C#X\<]LϥOn"ZRqIY /2n]_ͭN Cd3ń8>%틫Ajl|L` p~YQڻܜkO ^wE)WrRG󬲐Lygńf_YOPn#/c,R_7C$ G#lTBHSv)ǟ%&%آ9p&*Ey(y{nn,mPRH e:fT. e:%kHY(!6M"M(d6!ASE(SjX(ШJ}7mUJ.*OS5]f-;kٴ_4,\_tMԙp+(yɔ"h F63RԯYCy*J. ft)֑3fsgrIzgl|QmoV5<%w;5] pLw NlQ_7V^^f _to7ʫߑQǷ)2Yz੧\cG&G}L܌ױyX iv6fyQ$X*%^^UG-X6k$c b{czV/ J/W[c%}IBVSBU#,d -ӡo0Jz|JHx"S]7e)6a**njs^&N-˄sڤC pjkf)"[Ѳ11X/>1J$}V`ÿO_*p1LĔ|=0F%z2J-|2Aff,sfm%ۻ 5s%졒W'ɞ.=a*E~ޅ0&WA-vF). b"He`f2*4`UyEO5B=JU-TnMM"=kv>)ԗ=I[rlua>4Y;/e=K6J@X|9tr7„BẌ^ y5Ajtᘄ&hD4/!'DZHM*;~.tE֮GM5Y,SqIȈ, BsC^ 5*lM&@-k jG{a? SJ 0V}!!A@G`ٚqsPVF!?f#07qkӎpggHi (w)EC(ϔ׻]Ԡ*Eͧa?ZFe5z\{ '^KhP#;ETԨ+Og@Zs1 b`9{?Y,O-;6xJ8OBa]\8F'x46܅ ~> gڲo ;&ZYn ߖSQ٥\gک 6QUAMRN6CMФ0H$!*&luLu8 x=rc[gCv|i\_D=tK`~)T¹M Ye3]yOJYgX.Es. suzg9%\-q㐋ylrKJkFPi_Y6S^ׁ(LZXDG=b B>W;wP0sF2}&o4%oOt~$+q?P`9{A]@>A!i;93;^WMSۜ~}E/!Jm[)Ds#R]9E~\z]uĞҼ_+#J[L K/н%}BtA Ǫ:פ2b39o5ȳCaT`X4ȷI:h ,~?:Xg^,< 4m2hi%c\@@lW# Uc |缋'vl߹μ@{v<(i8+@r/Y .oضE%ʡ񜔣󁕤900b-pkzG`tC?y87%"M/_ j= 2lVl+L)9f:ǂsC"r3|-@rǙc#P!,L NwCWK\, 'R<0:E.rlYlufhjSybo@ͣ1῏,L pIɅЕ&8&dz8E. 4Ƒt9\9ttY=S^ 0@s& J"Vڬ<;1`wVloRl!G-h/ҩ&OnjܚewZ ļۋ&jgiSFI5H#$s ɤDx(fQ޿6tE$^ _7 FzC aYuv*- "PSM6vs+3[S.^f" =5!{b4 ZV0<^z8sIoZШ̳lf}_3q,c,nUH{'R{wv˯?Q#GL `q3 $8mޚLnbVC_HzA}=SBT h90;NAGЁV0,* K7xt]-t%r+F.f'wwcdWV}b⼆+q4MWOr9fI?Dջ/(l$JGVl>tp,rGdcϢZ3|tKdy+OB[~צސ5vr$^2$ҁ Jx #ˀ3FeF2jVSX xZKBcX.;!ܹD)YWr endstream endobj 925 0 obj << /Length1 2145 /Length2 12740 /Length3 0 /Length 14043 /Filter /FlateDecode >> stream x͹uPa7 50 .%n!G{Ξ[SoOwZ*2e5&Qs)P lPjK{:eN`g&1- ʉHE%4q%L\|V+˛ ';++/"@htzL= @uO / vva25q~-A@7q ?`?`.@{71Ll6 9@Yvc`{)kBCMRU F ]M@ |cbn2&u+cboQϨ5&#@ZCM+ ko2 .,'6ۛO[0Ej@kl vdv@ M[a\kO \**JI3eO™֟o( x [jdVb30ͥV?pm bٙGY$$/+.&$_\\-RY8,,3ٿ>Wg_4'GʁqW'?)鿣bѷ1q϶5wu?4{kw?,i7[eAsEQ y>+a}aI{sqng?-!z+ ɓ~{*ٛI7ՁE  _fo5C[L ``q>;Ͽcbg@wujshdt /\!m"|rpQ@&uX^s}6U0䪩s]__"TI\MfL%.rcR̶qގoW)mk0l_l1= ;;!]^MD Bq`z+L0gwtc}|W%Z\0>" 6(;$M kw9pLU)spQ|oQP["b-$)эاLAy' 6o>\b#ksz_ʕR[xv]aXW!n2k TGlgV}iL&5k^LÍݺ=C̳h1^jS" f.UN9ˁd/-]{f,`L,5wʦ[AkU^;2UҒSk5 .%4֪ ri.ʶyȢG<oA5"U q9?k1py͑hCXUgn, Au8YfHq{ M-fg{U=?Zc?kEt \xE2*yv byB$k8|JBJT[b촹)qRJ[cI$0I]UCaq'+CLB޶EHt b:.ҷH#L2~TR k2e{7 pՓ|Bmw648$>+um)=&L[g{rF>*ITLۍ_$ r|s! irY*+`.~56k`i/"14#KRȍc[XƙDLМL!)#vYʡasUwQtA9$j;ݬڞ6Ke='d8I$w;ַw+ :]-Y*$RՕ?FthvPg&lغyu62VQ":3PFʋ+=jaz֞aX%u-Ͷ:"n(M#L}vQ~VW[ؗpF\K^!׾ȅryߞm M25"$g.'%Suquwr-i{MW*Aa'- /эs/:zf9`w63n>`!QKFAʧqK~I`6voZ@rӵ<Uu4bey/;U+(_b"\J@@ǚ{kӫwR1]=[Y zBk'{z%#Zyr Л-Y.45igYd߁0(O0ٖבּlL Ugt$[wk Eϵv]Ptە' Q/eGG]&隈8a mH=0}"r~Q3&ѺWgWD_)ADzݰ7\y+zqa^6BB/,:`/8!KamIբɏ4[$8o$*#(i/ 3~C'xFaYc'_{Gh#׫y/jx}bH 6IwISxĤx!_dŸ3X\TTdoh bɿxѮ۬d*+犸զjŤ+sM+Uܭ'I$*TɅ2;/w\46}i>##߿rUaEΤՓ~PF5/kcDZus5 *$Hjr.Co@5Eu){PoGDJdpqϺ@O>OR3r8 ks- rTe\.o ^buիO7 =HBu4Y{Hɔ$!C^NZ}QVGdQq gQlɸ ߦb} 0АrM`lyм'Bq䘧{sMMGU5g*_D{bًأxQ}9|C0CCM nW\Iu}Ld]F2JCԜG3Eqc";nTmP#d ˬ)B#f[T[d(1kgng, X:ͼi5.NEZ_)'Cn,{9:[CoX+Kpk%-H@[Əqi+!9^/vGbq;"[NUO= h]߭e_}ӭ d=`,1SVT9%,r'E7}ڪqӜ &D5J2ԒWWNPݛgn*1YVi+GE6"9%l߆e lYgʌ`2s-=0T8sLj *eP>n?|d%gO?<GiachzZ20%)/"<;BC.w[e~XYXAŘ,9{:Pgw]6zf[ v]~v* -aLJ㳭msE|( -l9MDċ{B+a~ cd2_1,:^o$xctx=LJŐl[f~|Zx 91*D:?G{?fLgGzed%30iwV\h)4sDNޘ`,H̋2ҒmEР~ƯW7rJcόx=ٶvQҊ \pSfe@l?IN&򸳌L',{|rJik4en(Q W7@2&rH_S=T" qDѧMW#gAILW'0˥Fba^\e\z"e>9_,u5YAm6)M~]: <=6곝mUƪphZZGaœ^R㠌t~p]&& ] 9'q<41+l<]JGv (Ğ䋥^W=p`ze4 .qCK]|B7ZGwo6 B};L;ٚϼPAV2|8I9RR^\Q2[Tatr!q 9}N[`%_~JN`@wF eN_D|V_xiڋ*yqAυuFQpfB7TUQ )s/֜>HHUX=#{:(jaѰO7%!b1^cy"BaD k~L\֑C jRI;{_/eNx;/IΔDQ$*Cf VCFǝ:]־i35+.Qg;(ʞ(FkW`ۼlTփb5ґoȾZfl 3 ʻZ\Bfa37rR~G5gӏ Ōd ^^@quQA@@S7VnyRr tYLVLp7 %(97`?t.vW/p!_QV/\iNfYXHEJf/*5xF[j#KH=_\ m#1, 烬NyU==dltGD;\B򡎟/Ee8S1q+$3%|1(V!i5ۧJ0UZJmC,mC8%T4?_gu)'ܥ|PT9+ #DψtʷZ KIIeUszANGf&(:c2_9W^ NiJ63#\-ᮺGc?|"&|[Jh%#*>K<%sVǺ˟֐+NUa;hC7X/I!Ub҉J|~e> `5ҸS^Zvw.ϒ=cJ'2 Bz6*QtyFzjۭn].ˢfÔq (w?O~FbTއI633שMޱF͎`Քz[y'7KrK!%2n8ߐQKA,Lyx*kY"NcUR)5_9:IϨ-Yb1hzrfP%ݎSZ~ǝ&V8} >G_ <:2M?EtaqN-N)5Ez{X3vvJ}G*Կ (x`Ɨa_)fއʼnJ㴥h(1oIӨ a6/G#VVBI5P*6 $@LMOKH;׍lNQ;+GČ}B.xc},˱j~+sl ã$>h(M/]a0M LgFSw:'UΖ1RKsqFa_GF}Bbxek!p0O+y귽ytxJ='7ᶺ?k+rt5"^%LBx h2wL?q䗽tY31[k7%b(B-".ԥ OLQ* }5ǃ~[V8OgsN%s^oo;ma:E޹Tzŵ#8疉]N0Fψ#(:jIkL.,TM.YxPMU-Y'RNMn$0171~<~Έ1j&){W51w(傉B䱲4ڇKhrI]Aa(I"-,0*p1i ӣCcBEwup(aoQ>Wk H8%oR/"4`2h7Pr[}4% Kt޲~O@[xMU3eGwLzc M8)%&;9S*/' 4^8GB0; LK}.I}O/* 0eel)Z5}!jHp_+qQ5b[+IL g2jbPaTム rD%| Rڕ[2.59s8F%sb1EkA`>JAaxɇ_~\Ԥh|ICs"Aڅ@M黎(=줤~.q+ۼ\rFY*ka#ﳫ }ϡM&zfoog͒-ag^Shwa;]$p\C}E?N^9Z ا%TmÈ TpvLjx3A b]̺s*KoKzxӉjXww|ҶqvW*\ AO#:(2JQJnk_LcϤk߮  GuђS2d^Cѡ,Ɔ;TJYds&p0 \7;XWB2wM6FWlQX]TH`.$\E? p/j%++-j^@~v3Ƶaw ߂0e0=ݍXŖ4 wTd=+AhWTG{/iL“NTH%_hߙJ1W#`POǾ~ ƥr(ˊb/5Vh{Mnıq(͗H%л_K]c_b42}ߥrrR7Ƒ{!*U. ycHʝEQ!zⷐa[">O2mE?>ly_I4docwD?/ kxmpT^pnUF0vnB \j,/%%_Y2_#56"ةQ?Zh.;K |[ذ=Q\ ~\3S4o{.soIapgN;: 7 mVJPH: נּwGc8%L_Xٶk5X%eJXj9ca->`6eA]l`*!WX śǵt [Lj]hM[GN%Y_~'a͡^Uhऐ|) g4܌6o&-~݂c\ZI-|X]6%= xnx ]}K=)cbN }jΆ o&ނf=,U? nmÚ+rg*}Wu)HZhxv$HA} ȑ)[9Z4mwxgJN\Vq}`/'@H@2 :nSn TS~\i37b3~Q$8ڿ@3Y$3k DGpk!£]%߅ӘwMp@fɭ%X4g*W<[%]t#uN:!i^'TH ?V5>QɋRAba/ 0ry\S.*sUo$:O ֱ-&x:wl0oXyV(g7%#s*3&CL[,R[Xdxmr~7$Akn!>Ytu$֫`Xoԣ(Ah@u2"%j }rNizi>ƻ77"Ԍ3=?y߻f׌_=D^sGO/: rPngXCԊ&I\S5esg˂*ts.p6!11xUb 3BP=\Pp: 8iEs/$5#X?.1e-t*q189xO 1L (?eTp` NAO#X'Rc,<%^*m(T㞉?!&*Y_ 0ŏXar-_8gϝ_Xz|_Ex7]Z^\&t.TّU2SNҗ:!wN\gO|`pR3anKyJ<O1ێCD[?';8#]˺n%Bffa1_w¯#FN F9Z5]7k]A `]Vhh;I=l?aSr޹ո$JEͣ ,KM X""]Rdx$ʛ2pIo) 0zjn.7'6v& N 3+Ų&ުtUUo_x3^Uܰ;DIZ)8UJrKi2Qk:cJ|ie Őbi|x0G2G'TK١*t:$(HiiDzE9N*Xkę}ew Mv$/P-[d \Qbg鋁U E0H/eНIBUNl6-k.3'K5զEd;+c?by,S*x>gb(ʏD'kU{_5b)sťX9%~ljpS&$??Hh$Cm1\8\X5hjUq4o?M`p' I]>U9?"ɜⳑfPTR6{z"yk`A#bw  [8LY>LZt2GP$wo!q}Uy Y3i/ˤy'} fR?f"kgQi\L;0CcnKL]6b%QA?>ɯޮn%C"L3/hmUm!?cbg2]18Ar2wꊯj D4킱@}E_~C ?Aˇ9Z ͸C gG ec55mo MEMPANc_uޑ EGqVd hCqkzGʬґwf'rpV5AtFb'gM*]LPDSyr1Qh?S%ݲ$] -}l[x!#sDZ04qYhqm>BH| c.Z[X̙YgN]&ėK)վrtϢqHXŭ3WG5Y 7wsAZĸvB[D:m 2,f8բؖ D2Ws:P"K7zj4kE ,7v?TRHRmr5!.]dީl|,ZZ@ ^Qos ͤˏ5: d|yeją!IЈHkKtkW*C> stream xeP %4tpIpw4w ᐼk쵾Uﻻ (ucg5%@#2L OF&4tي:,Nc7+\d -`:"щhkfa zSٹ;X;A 7 hQaz`hkȃ\߄J-hnhm 1"PVPST[l m@ |chboIC[9chkNFQDTT$hBb:-@BME &sou91U!U-E1&0\|Zfᐿ͛7+=ΚUs@Vo5OmM k 0~%OP@C9!y)q1UlN8ݟ;9 EYLHTN쿜ga tSߖLJlaH[tߘ?L@gG]ud* ~Rę:,( 1}9C[7~NNΎ?Є"C#! z G?T iNX+oZ- r8oƷ5ؼvor93ѷz3S [߉81Z;D&+3:@Y66g$3%d05vz[߾= ]z3qL,m.ek p#~c?GA?mlA-_FZZ+5v4&,-܀&N۬ ٚYG{X ҷ11s[[lR3AE[ZYN濶⟫b  [^g:833mhLn@o rzS9;y./&ag0 A/0E\E 1$"_ `޸Eo\d7.rћwEo7ћ?͟_O/zb޼kA^5+ό3-㿞~ aa:9X0 (ӛ~h <ؘto1X9Mg9A<߂J>MBqckJB-Nafo}|Az}m 5>X5'TL\( n~,&4N&SNLS:+Wu:%6r(qM+י1%ɏ kNm:*#7v~K.a$[:="M؉jxOzOZ^n-g`S:CIjtS|o8WL=4t:Uz\0ńl Izt i+b|V1$iʎBA1 l)`[q_1?Ka56/*؋Pbs[[K4$r$x?[I%X3@$r2M(N^_D\YiW-eURGStgougF8LuVˁm҉wU p ݨ?~7Ls'0%{≀ZOr ;X˗ " $iR,YSiߥZa{:x]Ï6YQWsFrT~l2w C)9@^j`HA$eF ݤ^_{ϿNFqނ3ސ GT1sTP=J,a> Tx~ݰ;s؜?Z\oM 5+[E<gfT1%"#R1.F"Cù/OOm0??"+81j˫5[Q-/d^+V;&‘ٗeJtCtHeXH، <J(:˛PVwlk U7>2PʀKJä~dՑ9eҨ\*&NXz ?N]~:`1gks7IBQgT}Le76ZA;7_8Ӓ£N:w^iTg;?qġ/U= +7x:L Cظ 9Y=Csto3Do+hIsl}֘rEce'UA>[f_ pkhMckPjHU1,ip&w"3gfOXM5:(E E?Czk'}MpvXr:{$1/\p6yΒ\wвj VeK Hx }fV C~1}ИɿM_Ȩ\!YUI SӐAgtUK uQ ~oGcCpPًgCc ?;~N-T-ȌD9GGvE(PZE{9C:{s[uIӰxE?A| '$aW4beS(|C<q,J2]1뗉I> `e ](uQضp]߄7 HRߏعyCO!k) +:+cQ;3==Z? >|ash r>ǥ7d4W@+p."s{*;A#l)a93-#`f}ӧqK~>5\9P]E#p=!c[*U׀΅&1q V'ŧȕ#02ɈDtcu;Ӡ]/%Y-_hwA7ieß=C3TT_ЮnF& B|07~$I䖙19uxP$#<5jtWgﴎ ܫoe` Vjݴ 6?BX kNC"LLEeʉI8u]!5$qdE2KI|d}L5$ !K\#]v8]ZyK`fAu$X5nC5*`Eq|*dݷ[pމOlWOu:3nS4\u 9&UmfKԛ:)5;bSݒMbp_΃%H"v<:mB1Hzc^M˘GdE&WrX>FHZ͚¤c~v ܀C<+ԍLLjՃI4`qЏl.{O4О-Sv$޹2 D9w>6~h"K!ra }qPy|#_}K" "y HE^hHxv|%d5{ۢܧbMK+vPcqI9. 2Q汑 6.KQUkL"!LTuI▿8}zie@tT:VAΓJlP|z-C;~2Uy<o5|z a$@^="~3[Nە֦_YZRA=H h斴uc.e̩a9jGsz]Y hMKq ҉r{%ًdvI,eld&RRV-j$priL~NKUhMrpTǦ::Iu3!Iެqh`xQۺ )/RғҮp=Z*U 9ST1nXB$ǰv"Y5q+?mR&תǮYKHU#!+3F2ff/VQ/H]S 7kvkW Zt&aՌGU!|@iР3#q_9gm:{iɞ'L*Ȍdh;2b 0|%JjX]֕1jh^ly6bkdl;r|#~?#":DCqo,vmx۴Iuz?U~'8g~OAyO(1*WGzZ'@Gy:/> V+Q<Ĥ|m4h i/GB)pLsY+o~EۡBHzi^Mu+v1Z1or^C!NQw Ɔ;D~~ `{.ǘ0XtS|9) 4\1 h  9Qi}).a)+y,p||9klB`7(W}}QXQN$_a#Zv%QVh,3uke>uF~F@V j׋oY%g;HWIܿ>% fd]W8 -r5 |sѲVR>;2!fֽ3Jr!<4D@;^(> j^%n<C]#\2a:[\٥\%-/64;,fRQNd-=pMe֝CiUhAPsV׋1ICLE[_#'5;Ibl4qc#AIBuI_Ś4K ,>f5" C U XΠ;?G*\QV٭4N>3UnӽPuBÃB9rcJ}O,8-hliEcТ;LEU b_(s4OKQW>V[N,aImBǬ)P"bNXֶ(/v7sXˇtp%udK-\*|bUz:'[Nua0އ8ȹ@t-!+a>螓d#XPC@""],~vu6r"SUB-]8j{56҈On%WvH]HlYF0} ˘plх/aJ*l∸M '~cR{\/^ 9rBb#2c_# `N8†7S P~ꭙ'6B#J@wĔ,f\ޤ1"VjnS(CAɿ?(}mWÛ@vuƵtaPIı U>09k:RhjɿU9= :&V->=Ғu?l!ɳ@>1Zwvf0DZ(#ގ_x | & re0FQ  Bi0L/mXRV@Tnc[!bn!fNF-2(e?(AI*mgb|TlF%@=5ZSy儡$HsaHGEiZFl23ⳬ^xO]]0^SK?&/Q?ponoN8'!LD4Ȣgcp\ߨQ?5UF;dOAl:X tT>oUܿoP*ɾ %1u(54d8"N}&Bx{[t8u՝FY nkhwy,nmK Œ5b}4Ir < 1˯RwTk+㺌6wtj̀ r (\'c ^iM^6^nr/CޣflCC%>+z:z]ť38&wf[(1 >Jqs9"4-˨KzC]05ľ'˱׵},!Bd[ J{u=Ӵ*{S ŶͼR|̈-o?5?;}a=b$BwY0+W#0`[kk|h >\Mg R~Ʋ܎M:V)AXhcPvWS.xT#] jYGUtgY.16c.U@H!ܿԜ9 9bL+-tRfpps`(t^ R]@YpKZ?'Zڿׄz9zq#O;,Gy< g-?PQȌZ %iS$I:I%OJLHQڦf}{X3zm)EQܤ<)wꝧ#fV{7Teu_$"Ec @ j(rU1dQxyH^ЈkAիRT uvnHP^"K5O'=~j9$H]8J ~Eb:Tf\z< 8xtCpMU#v$coܺ$cd؛ExɮVq3Sx.*S{2ZGJ04#!QkN1#o5"-]vIsz^Kl׳ѸmJ|xԐCtBЌptPFr^^g&nAKvѲSiWz8Z)#C3>x)xM "RG~帟lZ%'ld\/)p*U!X#N%[@*c=&0f ?зHfV<:k&$gyYNiE?(onR/J2%x rR|>lG `2@n4 [〈?IϭnW/@>lǑJGPEƂf8I%j^vM8j[۫g X#׭C^:@X|C7V%[TTEHzzleůf0_pS1B?5@*ԋ`V7ˆeYbfƄ}A{@RIW(36zp8^+҃!$[3$!bxWMlM__˲ YlHE -bMȈ'9s1ƴkx^=폥W7#5k$\en%)??K"  &8'X7.ҕo;RDv)y-dFޡP7sֶ*f@`cHšに,:d8ce8-b| RyD-m7N1S iCIꏠf;N {o幌PiY9wIEW0Q?W^ydn05"3?kHQRTpD)Z9$LB ;]LS3\[J@U?2a\L,yD^ć˨Irm8NsXžI{.])9ߐAEyT y7S^R -Pg7wrwAU!uEI31⃞JL'kT0bs5qdݺnPӈozK3ⴊ9 ̊&kq_5BErؚXuv*Xר0p`]Hz >+ɶV;.X)m2nM( 1E=UN 1(L]^-ܠĠmwړՄ3W ïle˷U=`sTLAUJ~Qa ~pt"A+Ӎ]GSl#jci:݇פ%']CG5֜v)>۽v뵠ZQ_j#:Dwۈ'3!uF4ƆPDtzc d%_u+'3o%`=df}dFzֳ"hND";ˆh һP\nR`/D &a:0 Ө}d9)N6;$ @4 "o^5BCC#A3Fg+~)52l2 [f3|!oq_@@떃z\mXY~"Pqv}n2r&p=3H܋ ϻc _.O7嵧KG_,*)+okQs@)dl >ŧ(L?ߏxLMn{rsrT=]< ;txs)$=ɐ4>˨oX_ŗMb}zM['S&ݍ__?jg76Ml5" -_Fs@?$_yr|~#ȕ_ZxgpgӲr6'P3*;H3X^!ܵ#h4%keSqP>VxE]/zY;V;B`$'nW[ gfЛXreFcU< `OOw Q ʍ3LI/˽'ţLY-CO Рi0E e4hǪSaϷ)Ǯw= (Gʕ@.:n7H*7WT7D\g1TY$@x!hQ 77>g#2 ТI(j}KԮI2N^腌|rM:0n h&2alQ`8kIA1)3bHo8I)[OaQj40#$z=vK@u{K{ %BqJ{,IzYقͯ$L>F^|?V3%y |'[iHzGɐp˘w$:b>Pw=V9Ok'n[.Ay|4'[,vrL(fS7ky2cI˜iΪN^ErzuK~.$=qѱP*UAP\CJEVԚ!3#sh3r1I-3s6 !| Hg+uR=0v_$v b+25lpuBG;Ѹ!Ӽyh-'Ї}\7F?BBܦ>O <gҟ5~V/f%42*} 5̕#IG([ed R}K¯嘊V-Udև0_hԈD%xѶq%M{3 )la--_]Y[VTfȢfO5 Vgz.eUR LQP%SrDjy&GGdkܠ$P|jUy#>ExsՎ +/%F}4S.Y(+]<iu]=1 c7F4(|jxJu V(Y_f:~GxJ[bUB0/)K>daKppy%j tLHwF ]ugI %~À٨LDCYmfArzkS/0 lq' _M}_Sc03pBҰ @IT+W@;*"sԤNO'j&M-pu@BKЭğ&Y6D|7!4Kx:}?fR^_檘YufT.IZ3Ml")B8gaSw ,l(ޖU02i_޹-Fx+_F83"[rSzE23j/´ɜ 3*>t~<$ tPYWI7})F T ڈd4h8Dwp88`hu|p# ^ fqJ*eNшlST~Ë@`%ȀX6XgBu"F>$lIygOu0{U2*XI[S(] Tl *c|_e=^ ϏWH%,d.Vݻ1buH%Nq2} _:| *+`"au7X`k >}Waŷ{}U vw`Um47~'2N'6[2"xOW;V Nν+/m+Lz| 4BJO}n7_/ʢNodLCw9ZMz<qɔ0%9ZsT(?=ToOAPA4iH6}(Œ35,PcqeXmO>7<>z 6+Sf p[zWTB;W ajp6H"lE[]X,6gEz5=v$ɩHMlhНQ(U™pjyE9kB;8'0NEyn]hŐڳ--( }rJ!Ke+ZIZKѩ&l0}fDf1 ; U~xQؖ~('9[le%O<SBWXCr0g1#o+F|yQ0c T0T;C޶ bA8Aϝ콍@\--$B޽$rFxCR2{2M'r=sNzw"kQןn7<XtMi$C3nsu_׮]3dg\/wY'gB=q+3G ݍ|vcQcVUwK"Lz Ny$KV/ 8K.-g:u5wUI q;W2C<z{"uq̱x:o?ݚ-#zO[ 53QhL3a6Hj|WNTݗe=G(?¢$DW+Ѿ["`D\ekfaB30=҂ d7N޹D@ƧFCuY淅.q+%H0ĦWYQwQ@*?=$(WtK{ dlC2'`83Aoeōt<&@",O{_ X}L|i F$U>AMlǢ[ٿFUW)i=ڈK5I"Yde^Fovb!SF+n ַ_ꈠoCea4j`zI8-p=d,S5xy>%2p}Enx)/tKR4cT?R֒G㦼:IKbSC2| *4"0~,|TFwvk\2I dfU'4**F2EGj5 | }:6KcB2BvB|^WccI֕d+K ưy'0?p- 3&Cjj'y2!=^5K~W]<~yܷsꬄÇ$""- ʛ=/Ηe1trtJJ3:Mf+q'RF4k>43T.u oHW!3H<8-gWqMYz/—! A+, i:7n4usM=-lNKAhYr`.j,8LKx?IP=[)Sˏ_-w.gH#b)^ιǛL`V~&ړ|&Zt- sPҌqgf/h^S-nf,,j:Xw^."A^iz}~CyМ  }3O/Qlp)QM4VHJE48yW8il=| [Dpm0,&~r_2;+J]J2Bme1JTs[y {d*zD0ape!%DGd::o@:ķ7tRkd*zJx K |{y 7[S[ yrnV ؀OUXİNLnގU>8H>ph1:)7믰xspѩCO&YÀE`.@rIiĊAc fWP&haG^[Bbq,0 \G8i^B+=q)j hV/ו5-p;タQrVJ;4^8ݏ7'm8? x#/@BǑ2jnOpv&F ^ú\7]eYHg5,;F?mKVX=VE/fW`$ Aqpe^TrR m^¬Kz{Ctzz]jweM𰝱N<x+bcI)Ju*2ܟ6@n4U+~~W~YAp` _`>v:,Ϡ{ªLgMn K~^gl(.VwzۉjHl^| [qNCoҰ 3d>3;Ugd+Tm}$@Ⱥ3uQΝ3JdUkcTv^^ K74B~Td`ez1c4$7@R3+yb/뺺Ո=7>BzY7-[8٭)H ־TnFџt$ 1awۖ Xvfh m>+d3 Gd57b[;G7+ERm*x! p_m1 P/Rg1VzgLʌφTjҿ<'h|v+.-pDEUҳF o9REJu<|ngSwWJ+ xƸd_qth?C\|9zX`׼k/ "v J8` r{@fpÍ 6{s*Z9҃'kݨ\rI tx[I!-H cRwbĈGRyWwFyESems'[ymx_xы0\jc=Uj^t4Rn3(6W9g`ƷgZ1 .G H$0[Nm<>߂a:Rjm{wp!T+ژ x( И4"1&I?j΍T2#_-WĤh0eg][4k><ҬCb+ P endstream endobj 929 0 obj << /Length1 2826 /Length2 26829 /Length3 0 /Length 28408 /Filter /FlateDecode >> stream xuP߲ \ @pw \kapw ݂wKo;V}oQS꽻{y*ȉ@@Q=3@.jTY@F&&VDrra; ` 3t;X>!Ā@; tWv2 {z}{2H vٸڙ:N;d7hmj;@Rloaз6H20dA` d 0[@PQQT)ʩ+Q3T̀`a@ 72oq}k)ѷ6eSTr*a%e1:gAYeP LUYM  amd]FDYPYC^w5'pNLKX *S.FFgggG{ %ʦf` ; Oq ' # gQFPVBTDI\-W\Ѣ("YFZMhO~27Ҟ_+pIW51 [ 1Y3V{JEd%EdDa +(?Vfӷ6swpcF$*: hg[̿Wݿ!ѶtwշvwJC;+"*m 6{kfw~, >lL |EAVV`C ;W[Xc3kU90X:%>~ `mGДwo3o36 =nQ;G 9FfcQ.am |K3Tfdm ZcDFYxT~kQGKKYpg{{smSO05sɛ9OK8%L*/Kifzf^5% |ߌ"5$h!g!<};;}WD&dܙgA`"v6oӿ;Q0 A"8 f(%q 8 `4\  AZbew? 0ۢoo d06oWAvo1 i ojc kf4 Kd/g2v_;c,2 WIl`qIg+7:% /p?`s0U]g_`IF៷{C:Er u:Rpп8 vu+W.v?xJv -2vf.ZL[lۗH@򗷐ŝ@<.>rz<o ] Co A^"S%0Nʰ%cS?gm||R)@\_| 0-_6'ovdފf2,tPJfhΤĴTF?u`xEJ$.iYˆqΟcnİDwYBm[j #*\GpfV/'kT֦}5*Co2CT?֖8YBAR(dw^e_^և",` Սn2K)P|\V8{n+FCxI1ֺ[K8^I"h2_Kz*[ ]6w;,^4BA !TʬxgdzL 0Q]f®)rA~HXF{aoNjlE۪jh/my(6Y3Mq We9JvsjB4iBjIrP$4sZ)ɨߗF!7/\IFՙ (V10^^rJAޝB?D<=Q1\fR`EI6/{j:_a9)(,ɔo={ak*Dl'&"p>cFHKgob}7N4Pdr }4$9N3]o$FNiMHTDH b uwJ/])djwug~#3F*EK4K7wTZ0OrDx7] %$sCpZmʴxٷEq}@[waJ5D})'\tpMEAQJBr/d(dtޅU/7d-0@XSs- (r%W/0nUBj4_WP[9i>_Ujbmu,iY_7Φ8j#0;\i!+zt 8TRt<MFk+7}6В߭MWMkW'lܿg 1C̖p%nRky?VN$ya1("Y#C(DUfMD^ICTvXk bCӌSEɇD/KVW&\ʷ|5XXE2l{q좽T[B1?L~/>h0'UD< ,#ZsªCUK`tP[# ߝ!H76:kؘ59?=2Xns-Dã}mKxD!DT+ ~A=5*)uJ37OKЇnWZC3ZU$9`5ih$|ᖷ7̤!ΌQl2bAEDwm@Juwgin}{R LjvqW W̰ BM d2C*h!184Y(ݐoa? DscCbʿŬ KۄE+^^cGz0>?Ƴ>;df0=~хAÂy\&62MVrrS PM]6)`3*[ыdKD6?I^CKn|*4HlR*=E N+|Ya7l!pqar'լI8Cy-&:PWLǔPɓ V%pY~uKƥW[ cFem(t6RWh:jx^,qQ#>np9lNDr]7`*jV5q{}f`:ZFXUv8i?ÃEPXbT<+Qۃ]~.=`O|}8DMyD&.rY=>Y3' hOH;})M3>KÐ 9 =Pdڌ[7XPQPc-}O%Ml($w.(J|=Vâ75#,Da0RQ.SA˯c oKǁl.S'EG_{&޾%"0T 1.ЙmIe=1۔ˤ /ޜ{1O|R щ‰mrd"/|>xf0M mhzy:3Uȑ tSFˈժ~z. %7>ETFrzSOΎ0 2[;˛xF$$4o ij %83@6D}%K| )q?D!.A~Aݾ0 f4wM=aMgiXѢE*h]4r-|"ntK; wt}:Dhܬ-ӧޝz QEȌXT(Id7b!;mfanBF+FyrīcQfn#Zf>O1Asu]RSOxތA2>eg=oFND*SZzjz -~V]urOޠҐUP81?>#1%]φqWsm TO'GG^ ߥWWQ02PP%X "/WAo袎v̟Gj*GD>UNբxG:"7%|cN` 1 ZDkӾ%bj$y*bb{+eV'RTnws*tF$x0讀3cd0Dǔ@#LBliı_e÷@I%!_Nt` -yd fb>r%ñh:H*M6ɍJy#Ty%U)tHnF8݊UkܑUrQV >OrKE!#o١$OAux|sVBPڈk[!PN9x3 *TI6z}\zL[e:_uIkH\ gI+\1۵|֦ tI"2N)xҽ+_0G'apj0̱&aA¤m}bֹh6~$yL|ql'|\^oXI?K:(>םՌyu]&EgD ,}R)*KhIkxL.J'M:+M5 U7ld]_(`;g=;  lÏ?Թ.W 5!oMj=qdFd;n0ȃ^7y*+p(!9ԇa!HwmbIOy8\f(ԣ垱Ӽ꥕PjRگtꦨn\Ej,pwY;3r9޸w:]}}[?o&ݝ$M'ZZ\X>>Bbjh$GKEZ!:#X侢_1ayؠvDGBʦR%92܋p&b6~p]lӆ^jKZ{fDtA$D\s6@.W s";b>cFGƗ,L::tO/zv~` M欝>sp.I&("3-tKm-,Po'gKj彀M'QKK*diBO R&A!m~;*[*tΣH4y9Jk+@{Hb+x9ҽwrZp0JkZH@Py|0]m.H} !Hd7;$*{= TDeeb.6>Gvܑj]T|N! ~!^X!a2o ܉$X!!7{ 3l`k{Msj/ki)bZve FL}/|!Ac byODiRM(uAl6 ꎪons4P,*CX\ %(SQdV ԕlnũ9 yDńحRaBIB>Fl*0# $=y“Z(仒:N0FaġWDal LDUx-6$PyueɏtP!RŨÆK_#nWUx9>M)kg۪>ZhSFq:d !Qi"AWwHjc2Ӟ2 / BGu(f${$sWUM!@9Xvs*E 㕼GXyNd?#gP wk86_J+Wohv&tUա3|@gi":0:{~e#NvZzJ;9gz*'$a $ES^;fg%kd,d$l6M%,i%)_eMɴx@BovNL v7&[=Kn` O!Lܽހ"kN?  . aļ/ڑ?a];^!bkLMmZV$Q椩 d VhYlr2S95u(#0)SҹjɏҟmYȖL7%TklÈS|=JVm2a|}u:/SWn2@fDZ[T`lm.Hw?CBB-62*,rs9z/JOS)aoH<9c?zrA[Q<86Mf@_5~CpJ""=;7:<~Z(]K }Ϳb~7>mźmf~CỮن NPv!coWXp~fP:YfQr ha]L6icg-'3iI(Yidi U:=-g_l[%Ǥr}l\eO_oE^ kikGy9G5}K0 .C By?r_tp~c;PcZ]yr,(h!hf~w^?P[偞ɽs:+ݝi?ġBuē5\N![ sV1r*M&ŌچFޒ1>J !ދQw|9QOp&j2ܟS{Xs$~ny.<0?&ܔ ﵁+,SP1gc(OB E:dj߹Mr'$bXVi^;*d) H3eOmDA[,R5&·2l׆ZEϫU[Сx0(4vbG3uݴoXd? . b6?l6K!ǙOr3 F,sYfO{2)o0,WH:aw?m^;/੅a(ăxU\iRij#$o+2eTi?b7|L; 1$|eH pfAOo,Dx BAXej`&\&3;3%59A羉0gk,,7c!Űˎ̴ESD:3SqxЎl"uk2jMB^H5~OJ7!: \}ʅ!`CNi4E2aͤvm~Ǡ~.x!bftp>W}&3]}lsi)tr}q ZcǼ6[P/5w(&;ӳ[OLgH"mKzuhzsIן#|,0 O>;Im LA{,ߠw-/p~/JG㋲-DM7ea(.f猺m&NrRFKӜ\у'?߂gk׍UY#["*JBU 64[A6ck$sofS9 ^~̣;|'[-kȻyN[2)sh7W $[`ď{{/XLD݊rZcSL8O]{LaDF ={ǹg¿q_3i7EU~rC4|[ot{7SЛIT~%lFR&p? Gq+\j phȅIN~`^CmFUFEwfQx;4:+=s25KO2Nat^=@W/ ۚz* ;ӥ*B+s}᨜P̍懬lhV7pEHCl581[EbZ5NR `jr̂֨O)NHW&0&M90'V5?c tQ{wg6ԇƬ!lEϞ# +SnưywJj6N\ߣބJ^..յ4W4箌Sj:W߆U+ZQ2=.T32}6ij&ݰ*򊡒1(6fx8E^`h7,K.ba^M R |C&O*wnUhEyAgx?XIPNUg \1".d^C ߀\vAx 32ţAE?AQ Qx]?`@E)/6J|/؉ /nдSM349>ƤPξ@A~qw5%y;Rxz"q#;4(l Qm(;[°05r6Z*3 T0T0"TPH͑€ :HUgdΉqe̸rݥQ\ %ѢF+g;YGKȋ`z`T"ӹey:m&L2y Z`dg,#ܔ%wV"*AV#gݳď|pK{IXր@Ih| ڻ XdвnWnKA.l]kӰ1^"S {Z9#GB]үo`/2=o9sǘX{DCjq> d(Y'FW 1[دG6D>—5}(#νV KfX.@9v>{9i|nVC|~e{o&<ן1gs?n$E8"rӝ_.xwH]ຍ}Dl%) Ta{L҅N(%M;ju`rO EÇydYFE56B&`8IKzdD%i1r2t% “#b\T#eŅ0+싚?a_}$ `lO .racc8ZJն8Ow9|HiW19 lV9 )*o3(y}wU&= 3.⽨B9HZ,1R96hLP|5ĊY! Lަe:)> +t`:wXjQ-&b/=Ej (5 m$z'NP$\-L^ nS}(+ siSsӫZx3D8вci&7Vcf ,|=?=SnYJ.$&K<@mPriJ`2#4 FJ}D1&^)#K<-t `٧] Yܤ\.3yƢL }zlЬЭG2x" FZoOF8\i%JuNԢw5CC+]4&՜*YvZӍvx܊"F#N|dS-c[Ӣ 5Ikؠ")*J$CH sI?莆_P.j Qfc{$C̜\c>] -bw#LY5~%q[$˾Sn~qۦ?-Mu \ E7W&8#T45 ^H\̮͓' =po /^vL"AO P_]fy ָ :荲P\~TUŅdh] )qT(Sfkul2od[ڮrHu!w҈6Kh|ׅT$?HBS(zRu;7FGdvW`ׇrъhQILFsC~i*xQEsBU!cu,LT7fztWM$gv#/p}4#ˢ"v뺳&wYC`ūz o!bx2Cy]׈]B萾Ζ؉ !sLˠd" C2P@\Lz@硳tE;f{0yC*t'FH$4B.>Rr#yV@8"w֊L#_YMX7i+m׉hq\mKePnjeg: nT+nn(:"tfvͯqdx3iGbpk= zd)A;L!j%7(c_à 3(b{Ȍ_g$oTp0Pû'Gf"<bs$n&DZxqDkx4Q,"ƶB-F?7#\^zo.=v)"DE, $B#Yr^G^=0̫By_p(@%.AlGmEHDDd0PG/a|5\hɂ Kӯ|D^ <ו/lxCjB qmXt!8MXP]an)Է%GEJ;neJw6Ӻ"Y^JPpɖF~oS'ΞDYGj (Dmy"j #xRUYU4ށ'G+za*cP!ц:% *Z^C#/1*im hϺ J[w@4IlJGھNeEVjw/$Z^0<5v['"v,lO ҇欌 OOh{l^AP(4sgVQL:F2fzȾNSq@}_,Ӭ*ysM@3ZG>*oUĆ SĠ8h, ^sQi8f/xc8yZJ'U!n\F]V }}s4]_pR7WYU/V{`r,FBG,#q tn/$@yٻ7>qLz2 USk Aqy #%lQZeH_<ۭ[MKUELFL#uܶ,StQonism윁.[254i{LBo[[\LgRv ? xn5@"?Wn!NLAeGQPՊN[(4! s:PY34uDIs(y */r#GτųWdNӷ @ϡ.sJ [AhՈEc7aı/ۣzry4ȥ]X<"z#A~aQiy+KČ~~~#XƩ(ze-ҿ 3HfWoEƬ51Gxn? >p۸oIߺh wDt߭lfK(Fu-hQPs!ݴgT>0 Lu֥綵,ӋS>K _lG@|L졖B\ՐW9b' lCCAЮ:B ~ya-_Orr_11}@z%[9~~DdDNrF_>KvsuIPyi9FWّJmc̕^ B3o}3Zx ǝ+)xoq tw+\a2G*j)ɑk6 TQa:T7=E' A9{oGdP#V? L٤LOq<|w`X&|y(Mc|5 䍚uiUVT*LjZfϛ/'w/n@Nq&c?8-= {)zFXpI /&bW Q}P^,2 Z9-YnA>ؽ] r/*Ju6/K!N2EvvTQq?X?H"c ~:vdLy1'_7=7#!~_8ZxD&kƪ2Zw^躅 U0;I,=fIE'(HuMȇ fX3Jdh ,}ImmVtA 0`ww/bkݓC6dMZUTNg &-XԘt:`}}|~c;e}Ƥ{*͆8:B;b#V,f6}p` k|$uuő8v&pa}_)no*ڤpкJ,32^ׇCT#x=;h&I etpzM7ʌ\Ș:j9ܻDJΜYAr{E?"ξDΒ-F"4lwV)ȫ?qft0B9"lWzZi\a2)@qau3iq -T6.vF89^71bd\0#1q@:ctXrRqPg~IL]buCK9L ev$vΤZW_{}5x/>Gt{ҖuӒ7 T5bQvưόf4^qy\#jc/I]E|d׊fjAq"32]HB#2?T=[ e> XГALO_8Pc2fi, a3Hj|+\#yW3z\t$/$*C6xϨ!6𿘈jQov*!M"P^h!=$+1/jLߴu oǧ to2 zu]:k"e(/W&WV%{_zF9ofrf BW@\6(ʬiS?aVـJ׏9Q)D仏cDEOyD\M;p*\~]Rg#XOևAvC=.yh98V>n%>SayvB* ņtLVnm?q+'s\)ҪҫOVE"k v>-4`j(=y1cJdo:qz<nifh+L&<#9^z$eˈt>\U:쟍Gq;.9VO+lͽ A?Q3X5'h>e 1v%woOo_F~uުrJ;\n!Tl8Yy̴uEvڷ,j͝/-Zru/ЍtThBuqZI9 BAF\慹 ,BiD86ݞVH~Gܡu`/)H0zCb)Q2\֊:\񛲥uwdߎx >.;+Ac-A Cao{Dw=XP[Q W?ȟ`GH߻([Bl ΉY}`J9 5e|򋈗v יJ姦ؙx9XrgIqQ&3M/(_DoHПD3{_IvcL>E]9Cy+ 3.Zo !T5OSX&k5RD%NI ]m<<$ɟA.C]l/֤VNYemȻStXT@~t Iȗ[ֵ\z.ͭ2H)ZcAaZ]XL."9Izj!|[aqk*(?(Eo̎G8&L8iITpb^ϙ;VC4sQ + tRKfeXEhaY]O^}U3і* F6o ^` 4:9rD>s}"H.7 \GsAEU%,ʽ17vk۞+-N:N:Ήm۶mvNl۶my4aW#}Q Pf<"(7v_ZwRc< TDzS#6${Xg$ULs`͖B uM6ra(pҞwm|^Ezc);})"ThL0\եgbLv?I@`At{ x!UxZ4#יQ|/]mdOU2wBS{Z῁O6-ܻ5@ƚ&ECS56u#O✊_9}p,ߴZmּCpiC7Gjw٘iw%u큃Ü+,w]^` #t\!4#N,YHpfӟg-?w ۩u̐wHOPo9Mu%#I2 (OO:wjXݒ/yȞ 93q1[#&о^8h) ebvIp>8XjRVVqx0ѧ1\cʏVtl3D 풞_wL1bH֑6G%UWwK'z]2²;Xc)8pOGf1uS_HpJ{q"gզ*C{8;x4㗵ՄB_@`!d`7|?CAͻf!]f$Zr&`޿aF@1798p5j;8Ӵ~&$fųу=l {;_!y,)P:bJǭr[;d_Ѭv0M6AȠԦήYO^C:gbthcd̈́vNV.Lתu y#We=Y"Zׯt2x,֗9I #_D.@E#Vi#>J CF̾pu q?#lMM.,= } 2{>Ԅj'#$j=19Λ5#A^#tjg:Ȼ# @l~i; H P5d}9jk*zιF)B@VT9Oes"5 *!6(_ )2߫}Qw!5!P7 CfcV$˝$mjB'ɥ .fn{ z*Κw+=]p=&Q3ܐhLϾ8=%!a"3_d3p B9c1X ~ :DP+gmʅ2cAuG ?f757|Akg: t]šTU E.ýTֈgpt.v)m&5Yހ;[0cK% -/Ƅ9mO7~k PJLI:"̿BXˋE, RF576jԵ[[5fKiUxP 'eD1 gȹYf,V9Fh i{'.˩CySJ*eV 3[>@N}ssțBDzicYە3O6 qf1Mp +$W-" cݒxcU{Y&"a$d:hBt\QnPjDX)epAO A;ʑO<[Mfü9H2=yּZϒTҒ)L_E47`zք]|I rd MFmt]4@.b.ӧ`|2J7A]:~t {a]mE!̡UQ@?}Q42AƭT k!?Sb)y _ZPi]H}Y/Φ2kBKo!82sʋU)^HވėKQmpw'2|B'tg:ZS 7ceV@Ƣ 3*\O~'F0nJޡd=!*6A8~Bvzc(}=ybmcnn5^p'2ƱCyW"\$Xnkfr4&~P3Ľ-)ҐyFTGנU8= ͚z]M[ k,*pvXz y]J#Fx_FYyj)Qxnl9"ʬu8XB|-{2lCdlg#8ѧC/hG +lnki@`ȃX!WOL{i9M 15Ee0`q`)SzXcsYy]T.4<[r(kXp/5},Y;#={d%ZFa:B4S6m{ckv}ZW1"(J#Zä q <~> TMF@wt<ջ[7˨:6{#ng>w)s^H-i#Xي` &"O ۞Jk vVϪ\U~.ş%!h|Nݕ zcdj2`٧Xwy&U!̆}P[b)6M^x[ihnR61\"P3풢浐 qr{3Y>,аnePc! 7o;XHJ]!4^iʂ!{D 0ZxXO#I3LѦ2s'yÔw\9lθB.M9=ҁ..2)K85Zt%đA E!L_sṊV>۽ēI3FA!=bٞ4G]geg*gUjHzg{h/?#*o_BvR}7U*pUMH#YsuHJ}sftL0V+E^r+u1^wd!CSYS w3m^4 vrT7wC,e?GmCʥ3SZ?Tq/iRKqƣ CPS;kS UVYYZpD-ϝ14{;KKUo?ӫ7܁rQ"&ۉ"wc\|KP|9( _G6I;: gh^10 ||gC_唝?P} ͷ #CE "B"ft@L /J)ʥEdB 3| HGC2ԍ׎=`ɐ=A@)b"ڜu/x9znU] >5bD鋾Yx#Á[qf ֪t蝫(d;ol‘t#gh$+} FX5ӔϺpeY['њb/2e{ S2?P!#: %Du=+B?MxkyFZ-C!n^ɚܺ&AȬvbMލ*v݄a+e(²dk>wq3c v';U,w'?s` c@?-cV1V̴M4fS݄zJP{o6tMv#ʤEnr}o4<( d mj(y?)uNi;7|res4v՟QMt"vzG ְq|xQ-?Q.ko$mg vbczh]#4-mTok0`EBmi~$CmQM3s1y!QԽ"x%*Lr~勤a) RcΕ/*PyӂxS#[Yv|sT[.r)A.jg*%I ّǭW{b*ER s0:Te0E1q+Rjш偘05H_f-](.G ]r$)ic9R1ܱsHr2f5 '#/όvߤ)\=R"X m`kať;|4e퀶z0|Λ;nU,Dav `Sr%Ec8G[MܴNou%BbcQRUGWfK_vrw>q ##ef:(>dNc( WF (S])2ɒI"܇5uY$k ڻj*4i]8y/IdnĮVNt\u=GVa\@Yoff) ψrh芫1Mz~PXk- ߲,oz>4ULȄ1{wAβ;ˬ# d7|@\/5ڡ*?bM#{q 㳧IV!3uɌ3xiAA^ a{Et2[ƿA5[D޸梢R"Ә{i7m*/8N!ܷ6bkTeSo3AJ{= 9?L^ZF͓?a<6tSK ^,PKч[^ڰ{u()ONᖤu4>ͤTX5X{Ơr˽yUז ,3_<H`M$W )J<'ߵ* [Z7( Kz}Zk֣Ja*WiF>sHݩцZ@$GUElsju":/u( t7hQ7^"p$ UNV`1U4dz0xfѐf6W7m2.uc\(k >L xo.ۻx7E8øɗ'm' w`˟7ЌY`^b5z~%uŷӽϒ p1˝X\t>0@V \ ${ I!1Go4Z^Ƞq'SIL ˓ 䈅Εgkv9'n&q:t #3@g? Thj ٷʝ1`0ԯAW/bguM]oA}K#-S`sIF \?"Oo9ǫi@Ȋaa9}f6b"gZPZ(PV_uV%V*GrWV߾iI4QPd su"4][I#Y HIu6/ɝ˩pK㿽]Os~:P((@nUIZHmyͫQۙK!gvaVl4I9cB5&&q١i޶[ ߞ|踓B/6\PcO MX"9+ g&bYF 0+,y9bZ.ՐA̲kꐔ厦Ů?KO|p־,c+,vby妕m/0R]uXo ?oHYBFDtI?3TnF80Qa:sFqo=#MHȘ!ѓ| >eȑlTvVs}qt5z/ՀjvBaIĢTT't 4%m5+?PoۂX߫\>RYi1¦,~ű~cƖVsN~-d'؈ẽgWSd|,jC6O`2ig24F@Zx۾]8TMD˵v\P$R]Vd;YnHR˲˭~rVeCJ9W6p챗y:{ L{.e8Yty-B{M܈7o̵6)d<{'c~!#dDDk X6FFR9@KP,1w ~ !+Q 0\Y6y,*qDiȚӅ)$r:l[h@^N WbpWfj.HX9=)QI":-7(h0ʼn`!Tȷ_!TY[f6JH7X0z-?`W4-_7r˺C &F@ c@Uk>n9D!**mmh׆Jh?ºCpë'Kݩ?1<%$ Uٛ^PA,{:;q|&MZ=17fB4j? YrST)oTYAuFqFp.J`aMmcoPWw _$<47 4#} 3#V y)Ip#u-?j|[`xAޯ IH1&]^>0s%WKl/ߜnp6,F4b<+  Jku~17ఄ"Ŏ k 83tZmR,g*3=jÏ WID]zl:..%13Ho}W[4Y&rAw?➷@>0Qx ĶŇUxǦ o+68:D8b@ph4>¢ױƺ `՘ypJ')r}+murUCH)F,>H:&##VU"kO`xdեdfV_Fy)[9T#ȐLTW>{OJ@ MZ :?uJ[VTfxtzAK83Z5ƣppBB){{ |;dZݟ7S¥KݛBŧZQ4տb]GuWtjp b^5ҏD3oB*Q]?'TMi/2H{:gepKys"tY*h@:=S4_6D^D[Ŝ7aM7 폊o-h{q퉯C->ȷX;5dv+Hl(1~,f|!B:` *pkjeSOCQޥǢ[Y !_@Nn -zÿ/x~^G;)˖ҵJ] (+~4],հqU2N0*)|N:cg#9ڬg ︖aS\\ot MZ %㔌>\&(]redg𐩎bL\&}37{ĩT{scXǮjaKմn>MD<"-SQFKgўWR3[yJZR_YgWx4wBeEXBo(6Ksþw|%+0A^ -`*f d]y~ld E=N*Нa(.nBZDƧ`c& 7pi610t(SbgFe560Ԉo@ѫ[Rl)eq4JL0aL±g!MRC:h63*ݪ ,i梑_4g͂DZS 68pY Ot dBF@޳),vpIn -Ą\ Dm߶ϤGD~l Jμ8mBԷxԮlә2r,ɾ@vS(W jq휽=Su<:zdŜ oJfКk= Յka"5oKYMNGmT&8gٜ(J}`=ad?GpCl%~ 8B'Мk)GgXյaK%)XD>)%YyF !0)$@4۾.9޹;+$|WB8:WA]g'vu3xjU0TG5Ct|G@ˠ"4sEp-qT)X/hσSD4*3~mX)nsncq)"jŜ%}t 8oR]3ڪEhVD@Y(~i41M "gwȸ}67rEsye.,7C)ERnJ ̮NY&(O`L춏ڏڰ> stream x̹eT[6wwww`k$-; Npw33ə;wX~vU=%{ 'QR67J۹330ԀZN@5-Й^hjc`a`bb#'uXۉy.E=h79@ht)Mƞy@ePwv76rvv@j/~1zAQ9aXۻ;[[L2 {w@eo0Z٘r* TQTWRfhX]@&FNF&.@'g;oRFv5 1`QTuupwWVjt1a5qP FPP "34e./&$f(/(l@fNPY8023:0;38P fa t*)1.Dr9KP-%VWUUWn>_ ɋ//6@˗)ɖ6 ` lA1uO֠Ƹ"l73rc4vfRg_K(*I+W_G(_唔Fv33s1rquKz.: +5ݿ!bJG~&tvq#_@/_.@{ z@XTDTY:bwWv@iDpe@&~A5vw8}-̀8og#7Р:}T1sL-M\@t]ߪ!_g5@3 h`o?/6/e_So@S%K\mN%Ruـv334&v@gg* E_,!*Ev&v栁99y1F :@f`g280ܯ9`0 8"'Q70FF N&o "~#Vo`@\d#E7qQ@\@\~#E7qQ@\~#]7E@ѵA+~#J4@o9o+@Ac1Hd `boK~Ilm5@ o2?VA TJߐDW_wfV .ۻ:dbHos (7ߙZd`cHf%g5*oF 6o=(?2f-ЃfjPF gF?&_( ]dIqt.mᒙNj_3G7AֿhkidGAN-=~"lclcPaA5qp1ڹak\M-AНQlbg_@qpcA< 2@P$ߩ ETC^/ qH6R6U(v^ߏԃqS6~Ǜk79!1!oየo6҆.CCSK 87jKDZ3#Xe |PSn[֩SRQ[Y2)ͼ< \ LG6H>EْJkOT8D(dT.~I!0!6z_!:+bbGK=kO4}_Po28b5TD8^b큦Nta LV>(Ʒ˙sxO9<9`$W]Y!M9%Kɦ0Sڜ80WKӰ 0;n*=ア:t}V5H^$Їxe uZs| uh>rrdڨ,Ds)|'u`¯"e;tg.goC0;ތ=&o[kk})5Zu˚Gݐ̯@0wX^ Έ˝8sXcd1y/GȃJʐg՞Uq䙖Y(1#-.z"#K~eV-zQ ~x 1~aMs-[pTL|ca8DR1ipnip~hA#)v}o7At~]YHr#Ɔ"W>hեNH*0ɾ%q?*/s ba mY#/:OB)h;}eյ*[b JsgsΏ CGeE܍.Cl$TN #P,)~:jG D,G9ƫ!Q `:μj -LJ7Bu 5<.є+;K%NVOxuor/Țj:Grbjf;-jul12eۢ[ K:A^Ijaon˄먇BC*Rϭ!<._~Le'u-:Ѻ&FPAS@-{H,E=)G\kl?:٠i]_Ukm;đ*2&txŜAJ hkXzNCl=;}B޹h9)IWǻiB7|fGBGšЋH)|oˎ>)o<mP7)EџӴ?0 P%AJnzqf~ h_x'D{*3泞e%45U װK: vˉJ|^u#mqxb,kA`ѻ JR4#.oPFFa}gr^,`Np54o(9 \Ok5x"KQWK! 6(g4?Fg𨽗i_|< ǢjU(1c JCTAYT{g)_`e:-6ѦX@|L0ԨWo:e* Yۉ DY'6% ʈTqup.>x2](ֈl]w!xwzL,[>-+"%Žnc۶L"9mB|n?7hׂ{;gһ+SneXnYZf|u]3CƅcD ḁU0Hk輻cc ewJŽGk/J1p0N<#6|z$v}`H$-H:%y-M:dFMM-|ʙqPG~SPG>xr@T"zE.X1a=똯.Ue30w,mN^M2na jH .xN7pfM<> K7O?{I/nI]rBI{E\!Sڎ-qޕ_|azQ{^!"4͉^̒ ]>0) |?Xsj1츕j {>Z bI"^w KZjCK+uw7dý1WJ7:B{y/`Uh|/N5II&p,3iLhRB%r@W>}2&?3lټ*AJK^G*:/cmťT/KZ]~>S>gs3%=Z<2FFpqQ"D},.r#!Y@5G[t7-cv&^Ӱ*[s03psW+8?s4dB_3Gcqgb)M)0He8 m4|R~tH($b@:#~{R.'~UݶxűGbbfыfoKZ˶liFo"r)SޅoCӖZ󞗬kAJ9U+5@1DtjU<.gvDc__u"lE-v_G;jnѷRP0k;_v tS;Nj'rGuFM:0opylͥa'f# ՛h}t~|0ÝO+=Ly*uP λ vV'lJH~(üțo8rw !;ReY#hwj#ЏR?P8"(o¬'*0Qj[Ǚ<#~z9荒-h"p+hT+x"WlƷi..=SkTW>vآ$+=mKѭAo.m=ZJ$SC KOOzI~4J!$˨bI> Ei&# Ol.V{'+M4$2_]x¥Ύ vd0'F+y'2'NNEdR۟J)G;P'Lf쎨.ܛUp4S/\}I-LN#.N\X)-oEҏ& 7013dJ?0c(rybS}[xvHYij{hM #x T&}(8_:e|k>ys}iG ru]χGfy7독<EԮ7;e$8:7p_Q4Z&'VnVbzgbȑEpۙUohpb'WiAھMPqZ Svd+qE͌L[F2Hrp1̠l6eSW#|l.]cCdQ\g=i)CbMbua8fz﬌gF5@ܓ(V^i V\ӱ[#EMn63 zruBkY׭i9Sz.$F⨃ϟOnVߜ@Ȕ`ʞ |Yyjw_/b]M}!XƂ {1; zݍp\zt' }(dgn-$Mgv04Lj`1:>ݢmHwV> Ɨoo/9sG;DzQIsQ}$&dqIl $|^)r (WN>p Dɾ4KېIь'NU*¼ wBL-cbUrPv<*R-ɧyLZDJhGCd7E { a6R2{$-O5lh, վNӭD,[Lo p.$}{{YVVKKP 09dSD +Sr'Gz *^ 41WRfLAs}M{xg^aSOIOTm5Y~iF8) A[žv@!_nv$okrxlD݊lxdkџG况~|6!PSDD3_~R[&Yj񃛐, rqvK䚽2Y/ 1IDJ^J[@M 2;^FeM\#ژ 3%Sm"+QK.fiݍCsG7̻mt<1<"_z}ڗ! :R.RJ. ,b.>7*1:ݜ,93(`v*G_dF@@iPMǦ]JQde[%>R6 JN9?UԎ⩉4qȸ^GYPaU2g)+YnJO6 6W1%c:uv uyVZWY\ƔpR]ɾ@l9`nI0r8**wt*mpRݱޓt2y50".9yiɺBlKKj S.C* *X ؠ4$Ca1A^V _Hs91F=.Ѫ-HQ0fU]2t}sOY_@} UgvWkT9VhiuGO,y-&Txs<:b5.[&`ca;x9!{RXᄛ$vAB3j9*7 ~)%ajҥ<([UQQؗ§~f>xNa4|"r$[ UU~V:j >9 Jrt3 2&͟_“{{W .v`iǤgts,6ԁ|zS'EDUgOCBڏt ,Z+[+4>C9D.y "6jIlz7& ]PA_Sd @"1M<9g;*,/q'EZH kaå^Qjի9Y R ZqI˸%#o37 u(r`ebD#vsbb t+047Wx؉gBߝf> yl-SȥiC0{ w-h2Zz:{lU8P ]C؉nwj1mͰY9@J=vU}*) r'r ?mϨ#j(`!*_e5'"R+z 6ףUжJyQ Y'0ɘ@9% 460_L O]́yjjl'DwXEe)3 KG:`g2(MxMA紖jOY ;jJf#A6=Ck~C0 ZM=996+7I)yJC%gIhfׇhe`7ȟy{U8GOa萻Yf$ 0lj+I3̍bCܑpKTi8ᤎOFJad Q.L!qI þw4)7uF1Ȣb2"Eg-r?AODY(?䫎DpN-v☞Vfl"ۛ(j欅Agغs:b 5dp<)-U"q\$Rho-4Wp'wT^ u:$2+X`۰8d<0<[w뜅I Yw03- 8D0k ƜgjqOƃU*}a#L,(gIѳ-ߵ24PkoZ bI[ tz. ?p)M0eC!뢧> W,6a7Bpucc܇|>R}7J >o]&:B,_^9 rnT;ւ"˧m7WJ:#e0X?Sp [I?}$QmJD=W]ZWcC}vz]Yb3N^ق]}DSðXSN O}zZhwnj[f1o-tל(=ǰ1*=O{ҟE k&16ǝ3M`#GϑTZ(`rHХg;h7L7Vf}*$2SVሮ6!mT;>2Ζ W2MJ^?"zQϛ oEڭFB0Lۓ$_]~ABTWy`|XzHbw)ɂ%ZKI+p0)=$#?]PVijk\Rf)-iɤI.(#,vʍg ұW}\疞PUq-uvk:3IHD*-it]=^}㸁x;S٩{@gؑUy/E^9ƃ"^B K; {.lXOuOX~v@pdDnOpLǷ=+P*rژ~s1<87{ Be:rbu/\3(+7/(-&\sd F2 ݉^hŜ$y!mQ4#D0dCT+5ar5m3B`¤ 5m3q{o̓ nϏ_ԉ$@?,j4懍]/WKї' y˻j(w8 2Cirݤ Dd|b#^{2'@H9\Eٶ(ljܫ+tJwu'xS_x:Uh>rŦ;rc}΄zx WyfG$zʊQx}?'G= Ԕ=.yٰ Ni!'(XZ!1(7$mU8\Kl7w.{)1y-47;Fڹ|Y]Qyi(iˀlHb0Wp9,]pCyR\~㳽[VPpe໣X/ kc™掽~Đ,kwoUC-MCM^q&ŞOj!L1H+d;T$Ǝ~:s<72a׺H /įmSoJ̱rk$?P+৻ JXA+}v3ӊ kPcm)u,=o%(| >w6O/1 ut2]t>Ǿwo(Α,WH{KApX UgwD"'ѐ}ӝDˀ 1lYO|^Txb GbuW#UYo؅|՜2޶CIمa0D;-eW$8Hwґ8̴HG tjx[٩î+6&{IA@ ̸N$%u~3ԛH[Bn4+z%('^>`?DP5nwpپ?#IF l*ՈxՐ0ćVV^^_zǪ+͐;3]z&A Vc$ YE0Wq㠆I!\zQUgl%PVoT& gʕ`UH5QO<܋$ajZ!Jۓf$Aj9܇CCݸH6MyMr YI݈T:!b,-Zle*מ9lTpƦJWbqlOXa~;:B*@d{]ï:}6Aφ82ƽ鑿7D@;Ձc)/&.yc+I]x17+1)lå74폣ve4<A:>b&?38=G!uL/D@ibP{Oޤ U@ ۓi^^ 5*s (YrL~Vf2ӼǬOu踫a'jG}\B<*KxӋkq3L51 obP1VA;,^ &}fx-lZ _a"g47a|s9|ŨfcÑAz#Rw;g4$rNMuPݏI/nԃܒȟ?I [ZƝUQ柴lDtX-vیH8>zCtF]{}- u#U:ߤmGVm~KxS!qٍa8JQpAqU{'ݫLϐE3I!7uxI ^65~l4 ߽L'T6aF_>KIۇ@XҌ6&^ 7 pڑ(c|؞ӏ >Kp+hKˆ?YöUMm30kÃZ߮x[#%f VC4Jn^e28hoq9>mӶX^u޸zȴ{O7,g$WGk9͇{"NfPV-!,PbjeG珮cԮ~=VPw(!~٩-Fǵ:'al|żL(Yf!(dcy:K@p5r4*`ɉ|x iO'}UEYXdCu= Ք0IGTeC| 1wOFNHB8 -xb3hG>wmYe;|G# qŚx,,-,㜽3/w6aiw`]_^kR"gp10}Uc`֧-(^U܆*8[%s}YHΕf.9:҉]~z@D |d͍",\^HȊP/zc~diU.]6Gz7!X|w"Vm0gJ8L4uzS[;UF#:4~Nj>;f($u5k[N jyRs2)L+<҅RrѸkRF Xh7hyOޫ1> `Ix1^HUcک͉ݱB'O{xN=޷hKPQ2vpi:G"_9$~e/rwS꧗1r.Ts;fⷒO8NF(:ƾNOg, VvK>RHgM6x= TKILA270du_ ^ ;#_ Ȏ@KаP~`5ѹH?muf`w,Tʱ9tB.exJ}n+V+qjtDeK* 0,wJչ|;5#B((Mqh)mpunp~$Ci=`-wr#K&"2eQB^̺$VoɈ)zX{~7s(:X~ܩMi.LO~ā^Ӥ'nN?>Ƌ掟QKaݤ^ø߾9*e.Te0+g. "R z zb_>Ys1@S Mfu)\{1VW~25I!f:1;SvLn&FdmORѝ(9Tn;/7 m9уd˴,`o )ZO~P}*cם̶a~RaAo䷅B"`! 'K=ArrxDu7Y G 9]ׂc̼5ltME蔗MSy$!&ڄ%Kpmϴ Kf͔*L$r0ΞFT';?h vU2I`cdO@]",>Baa 6}<8Նtom [R'`hxLjtsК @qn|wj!rYqCl(X ֑R<_Cm4me90RuJF9?V9_m϶|$(x)K" ;/@Z&M/8nkDȤU];^еSq6Ø&$^WYge|UV{Am)햊v,< gE[F}ӌI }} /}~8; oY [>8?WҟOj|nxW=YY[EgM8K>"BV4%]hhJ暄]Ht8"+FIr.0+44.z܉4BiV3OKqT,d| NvxG0xLU]~`)ST {r pI ?F耶\k&>$}v^-'4.2<5Cl%h?H$G5  Y!,?43']WdG#+lkXNU)P{ḿK$ĸ[i೺&C0N1JsYT9ζAO_CD&KzVT>A+ nl]M] sXɥ{\HwdK~%+ A~yuˋ3[ŦeždZn,PMꡃ[ ʟ1n;u .ݑd 2I k˻Yxבi̓P_eMtȴ,nP9H!qRb`Ebv:|/fZ4֙Nf xr݋@~]k&9NϺW"!o#%wb.#&"mW?~0vP ܌nBme OQLi=#Ty8Ҩ-h*00 ?l^_$,1rJ_G-[1 <4Ϟn5n7 BI9rԤ+s)B+Wbl~p .g JLØh@-N4OYȊ A]Ԕć<ՎAeo{5.$` Yl!u=S+= Zu&#mm%4n>߰Α\cn ÿ=T D#73"NaEA^i a^j)>7h_l6A7aWPd~{<ؑaE@<{w[! Kc{VSwTF|ZHf7iatˇKRdȴoEcp ʑ8l,ڗuDcH{r 9.K4'q/EeeW`$O(WYT :k7=D`A<(\nL؂w$:HA\my XjqFRaW*Z@Z[Φ>}R4/X!Ys) ]K_سdOy!-)U)bJPןx1+Pm11*ЧZ9fa)P._ـ)oCā8Rd Mc+yKXXRlӾ{D|=p$?͵HIpp= j 2qjE9ChlI&z~fW(I=p|=2/;5[ad3 6Q| }gΜ%&20ק[PZr@YGۤ{63 G]z9HnHuҬ{aŐ@/PFFt4SO6W=TO6vB~Ug|F7-fq1fL&Y]5ӧBpp>s4)ZJ_Vs`f :An*4f VW U3chzs(?-=QI}:d]6veY,L܍:ϭQD f īʕ깥?(!+O1PB[PUTь\& ]Pq #}٦+HNAEe C>סȏ.)wNs6M/_9,GFjkըڵ*{! v;B'WgsJ+) c&y _`%&`p8~R@,3{5:!3`|,`ӫE4&[+L?B>-a+alӾkUk~it#~ k&?'<@Ϸ2//ߥdCbg_ !# `!07rXxXo ubFaX7 4uZ3Uo'5f_" T/wY XY!\b P"?I= eʬeeM5祿w/4:TĢn0b,6ּ2 f€/rx5?*7ݒla/K {mu a?= cg#*PK* = ^eV(F&\&ij;`_ cQGr  /5yU7{ljdEK_]꾵p="i;Y%*%hQm W tLrs1^NB,(cWCN(縝#8 gpb"9~xnS4 *-r1 Sft(.g{4*Ie62]x)|)Qi Jd-fOWg+vh 8i=-<ov={Z-W,2;{ۿOZ8g2f Y [ՠ`~%10]>~(M3')q@Qzզg& EzR%apz{~eļbt `8 (^~zM)nni-gl6$ {!]!S\jvޙ], /,%(67S"Kј7x.K^b.C*z٩F]07nR+ow~=qWbřd8SoȟkGP[)Kb66Ow= Ax݌RXFnD41:~\fojAaY? ;_ZUd^L6бPJ9j4&! \T)7s`HRzE>|:k]D.$[ ݂+X{4_5VġBƑGL_)2|m,ȃj|Rfᄽ(섷m1TD1/5|;)\e$O@drh,S%8&H1%LycX۳tyc',6q󪂐:g/gW,\r&Ȩ6Fw A*/̝Qc? ;.S;{R/)@]q<9 r-iduTb])SDk.h,Ϭ?yL' k; h$B3`TkmA0BO]nRIz7cjV>*y"F$#n*jMz?0+IJV~nlE{5b%p.H?"9e2 3eVI]pJ"ej|B 4`fwJn{Vto{I@~ Gu{WIrlʴα4(8*p ,'R*+A hY& 7>z '?V-~ 4:.MmY)6aˈh[!=/!}ǡn4Sk,q%2Y5P ͤZؙ [Ys@ۘnIqP/ A1HpR-!2cgqjCsʫ|@}H_~խ/fFK?Oߵ4pJDZb$ _x}+(Pn+tm? jߙFY\pN +ǃG2X!י fAga jF?_=-wȃ 00A[V>ķȭ0aM+uSsEDpuUcF6 Y+4 c^ҭD3{J-,CT}3<MR lA}ab_Ef2W% *,?mwڹq&pJck!hưP8OCهܱ |c`E>^am&LWQ\Okxs { 5M"חY1o)SzL}>SYt5:`6Oz!=%Dq9Y_^\,ywm'`uxTÅUD,R}ߎ5/dPZ6Yׄ#9é}#_R+:4&(oA|} !frrg=@K1\+@\$o3o)6ZWE,z1G5FnMFJMmTV5#U0OPp^ &s}B;gw+KDХL:0gF"5̡,ǷmTR]lac=%ˬQc`4kÄݒ)npOZ.f &@UxR MK\ ~Tr f[Ҟ0ث`40|gMP6p歍M-!9 q(?]hܬ7LŏSQn3H2Ze&h3N4p']X[FX_LոxPJD S'N!0 `G.}XFSrm )T`g-Ԁ9H7N]( u)̲UE@wt\tBlwE~[Z5ɜm18?juszknFut."]ovlbлTل|Es lrzǞ|ɮCOї~J׷yi"XWX8¬a ӦF\%Ő(smd_ "uu3)ށ%c ߮ +!)zĥ/yʞXnZBO$dCgrOr~Y7j Lt g^#l`ZzԮZG_3kH&mpj;dnPYn ހ9TIymsl3[:veX'Of7VDix9܉{3pߖ<ȇ@. 祫'>ijx}rc/kwSvpy,'zkTϛpeQaͩ}y#+CQ* xwwͩNK5B}&OtT׼p[&@y·eFubpb-HC$/ih)"N +ʣ 'J*U$aVDs>oo$V(oLS ZO`ͰHLq 20o\lWQ;ٵa rŲKL{OGf/ y8 s66Jd%ǪYI ;dh otT 9TO<KUWt-&D v|Xܥg~nOGP9=E6:?AO&~r+;MfP:-7mtC sHЖ47!q=2%~ijSL%dQl u$dGG1렶l2?.X=vS# ՛GE $-j疭xlV5f3ԬN,|&@9_ % kPxz3w( YZD%ԍi/ bq4c652$O2aуL῏ EtwqM5^-5;yOL'JM&CAO&~r+;MeYp~ ;J%TyIM6{ Ϗ,.&}!]_-a3nAUDU3UɡeZ@a7bZ6ɶW?YDăwtBQ;Xy5}̗-m۲%6OuwE}lIۺ2i2 ,]_$33nՈ^ ]vJ{q}]QeƄ$QDHY2]{9ׂy\x[k@V5wvOA\uMQ2@* =7ЙeD:ݨ+ӟ({ZJM\ǡ{XO ަR_B D;_XGlE)+Od Pw `g58u_\3;9ŀNɧ䲊R46/ՕrP-z/n2c\`("@Jw-|ȁ/d È]cDNALX%D5 V7hg,b 8E6l=}OC5s;-tf~`pNhFǕ3jJٰH+,mnRHźi [jݯi(ijڞ"yWy''k_W^1W}(G[oT밶[eQ\A!YZ!+ BN6tY,PYJ-Z+UI<\k(_}6fmw@modПm(pzz1p̆QY Pr.8,%L,񒌻 S*OXNyCܹ ٠ƒs@"eß ៚ţnloV(˺Y\,ܠ|F=᰹ԻtVu~qY E;<Y0c-z]M腷Ŭ!n}:[)]m3rt/[(]o2sȄ4U(XN"Uݹ^s#*Cg09DA >lmό:6_eFȜ[#I~ aЛ"eJn&ۘSm{ 6>DBK Jުw"?qJR -616&:\tG^ wcT!]?h}3ImIR -OLTDvʧ(Df!N^72\eRнa$tmgy$C|ROf |ѽ]`,: H5:A!Po֧=1qHGdC?ї?t1?IO;>WOƮfl~2堙6]${7]ՃR(R3e8s kzP^hTNB. (,3')CqA`RxN9pI*mD:-YT(23x\WZtm0Mvv??Q&dPw[3ĭP:RλNkU|ׯu{hQq 4D%ϝc.GpZ.6 )|0b49n x-:*KFb\WRԘsfd˱9 MU;Xg4PԳm>[> ]?nc:*MN3RNt6p쒀ϝ' OF$RZE om 7Yz)NcJx6l]x.*e"(2fC-Mt# WއC~Li\XubUq08U5b :wFf0ģP#1= `LJp r[˻PBRJn V]Nm (M6neQuGDW(Kg0sun8By_F:*D\A>D@7 CaLsTI/ʀ)VTE]ybr; @Za Y6KirԒCsYy'X4B?#ښ% .|&rbܲI( H.K7 L7pQ!Bfuki`oXX+iE!dU6E ]}VrG #p5-VW iIdG1D*$smJVjHVUe󾺝X"14e#x(%\'Igmz?S[wut-%'xM$ēHKwHAyTTB@x[`G|A>T ( z!= @Q?tq2nR`r#_@E,.@3P%zDJJN FFX6T˫=Q3C#_ !MИϧJ@mWz,^_K"-ۚH*$~^Pْlg6}0'TrfJ O@a"?Y7T}x)e`rx1Dppr+ ])<޵1u@B˜tiHblLR\W+*ɜ/q%|$6Rf%UZ? ؖ =Y`]uYA6v89OgɏM s5s@eta>Ҫ."S<>2X ̲<#2eG?*yا CizEݝQW<ë~y6"zq{x^U7*1uoU rl[ړk*d+XVqvo%iR7@l>#9g4ljF+ɱi1S)BB _o4׻me r\2Y yR =92eU*spH*b¸(q/nL@ΰ<[lͿm#@'ԃmCo%_d[U_L|('7Ō" VFeF.'(bҎ*]H=ͭ4p889=0{-Yl (Ttc¢íNmC2 no߲T"BѠV>~6d:FqZEM˸HbV:(is[iG>5xyQ"5g,=io ÑBI~> oΩ?)P,uBÄz:~R%HN6QjVn~D2#z'Hh.[\ct%SgXl]؃@jEP| SQީLP]a6m>-X37N $Gܔ9/-2%#C1kq!J #7Zn;Ilamj ip X<&J-}-j۪96F.9)fy?*YhUǸ,^Y9\ qVlw ]V2J"L'yKe$:~L\_<绊b.jTL(Gqj;ʀA_ [*ެxE:n8+&'u4bhv}=wf),%=,/}UY+ڵbEc;bL+y-yv~۴ĦuDRznD_;8&7~}ֱd4:fJ&K7B@ .Kt R M6%Z J> [@zX- _ڗNx>,AAcvAt]4I(%aq/Zzj[ K} soMBgLv \jN!.~1N#?yV3x";x68rHajs2H٥F"k DUT+$}ӳ d?ʸT^gHfZ[\d_2<0ؕS׳|tML+6G"cr>:T%a=3H-}ڱqK#NxVKp6L|{#kDUcQ$Uˮq0˕֪N,â,#ydZ&䡕h]l|[ t+1$gtX31Xq%? ÆG8uu[u!L|\ . ^qA6ǶLHo@Ҥxѥ:E+bh _/` ]T3ͤ?IPL8L`kF8ƕ"G(Ī Pwy_|~B(_2:H0:eE-{d_% @7-2cM8Tmn E$]E6'aa h T>cbƑNY&֪ 8G^p+ 4[Tm4߽R +o^T=ӅLŦ;h endstream endobj 933 0 obj << /Length1 2844 /Length2 24030 /Length3 0 /Length 25576 /Filter /FlateDecode >> stream xuP[ !иkpwwo5KNpw v|S?oQ/Z{,- QRe6s0J8ػ202ԀZ^@5ЅAOA! 4vr3v\-Vffx $ RL@Wc5/G /`bR-4-^Vmp260wpM wyumT cljbc0703<@B+=hilkp0˄*@REQ]Iat7Z;]@c3yK,ޒDQUUS +IuU5zH7)jjJ,L`|Zf_P/jKWWG^&&F 7WFg FG[,@188@@[_)v7_~ ge %丽 j l1N8ߵgtt+qa1yMW~2օo F T߄mpAwxL .Lԅw$  j rҢ tpcW#u.QSR[ك@ HN: ;4ݿ! GRo˴-HۤhV94fvv .B T!Wg/96>[gneo;37G&u{+'7,f 4ΦL5 ,Š8:8̍m]~V@;M݀~>T'gYFte]_kBN4qf`oj\sx&WPP@/n @w-ZtV.V@3%+WS˿ko@ BG-h.@'bf/Mm..NοT@P7YDE%g3Tcggc/xfPrp|X@scL-G7W&&ߢ'`xLF\&? $d M?dS0)q3 M?dSV@V O#??b!P]ml=VLq:؂/Z3A93YA̭nb߯3P, AYrHO[aGymGbh (:@a?@VH (@?DtGYعh,, #!cCE[c,v ?bwuϡU})u3{ :;5@X"o j3O]f ~M?P\[DӇ4 \g?}qu;W&]v0hV/y [PK&klX)CQP[`6ej}G,_)3wfB=42ڥDQX74ROŋ,ζ螫oϘ1_i ð'd`{1:wlq?Uv<_Q\ܵe͋m x`.@/rEs]O~?>޽鵮@ts nk썏:s#ED*t\ԪBEa=@;uS$GU_Td[ByI7zWH@1VQ3hj$;~! ZR}WM1kВ.MZQ+n\!"w4!a #Eu4YLvR͔ЮX ^u~E ަ+'vSD괥3z 7n W.LĎ?$gbE6i_wۢ43 wOkKŒ9bg0`V eՒwM|,A6k%=ׄ >@`CBHOa `.U=DV?yy?2Hȯ?xfHlL!h QȋwV-I˷W1S PorolٶbR'1}A!Ac߬n86PYRo UrȰ򡷆19"~Q6ېTtO *KyliHBrC 0Nm@;]P3exՙ]ϴ6ޜTl;2 Qߋxt

=kQ.Tx3Ux%,m vH4շ}^ GSԋ慓&uΥma]9 # BQJT4ͲOtEL]4F,\y(Yel`-agjs>ʫu}~緟hm 4녤JwDJ߳wWkC#Wy}e$y׿ZW08T'p􉲴4335Ģz8Y`nGr"lkZp []!Y $ j1ֻ/gI qEɣdo1TШ_2g 2SLEˬ+CGV1ǐԟP|lHh3=MB˓\ҶR&[{ r(-%JW^"wQ:ɏ{|lu"to#(Efb +IJm,\Ss:de&%"W x bR/W(rAY5{c@cm2%1[t hL|';?k aY="=-)AhBx"#'D_ᨕO*Fb.8)>%<- mϷ2ߤX5鷑*A_ PW O-K(;Ҥa#-ʖ4՜l a(VhI /g`:+׼5S)|q(s CLap41adOS/axj7*iy)1 .8,; Dt.>ot#ȋ~#J+?R_ _o>]x%њGaݩglJjS. 2gKQѫ A-ߍ辿{z6AMZ#l.NF'PŰp#>]M#bH yp 5"q:.)LT9׫c^xCޒ:ebt-1]NiwM?[$PehZd;`Ǒ#u뢝oRoj&z&Z/%mHi_AF)1?1yaH!&1fהv-{h,3E~;{mWF&P yCT4u+ K #/QvGL}C$^GHfڠ5e-jy`桘 w/U#3]c6_]ÃޠW VUL |hU4'_^!/! %`ձ* `5~ޫ{Oz]Qcd◘D_f {}bNpTCf-;m4g|ƶ <u";X 6DO-G.̧~X}F&0> hU{XHhWXυ#E~AIX쳟0:Yt>!!h GeS܆?sI?f3;WEoch2'uD. ]Hy:4.ΉIAGsD8طY9S0NsiUƲ˧ <:ڨpOtC¢¤1WV 54euM*d\{^'}JNSE l=FS8I$mNZ+ʋj秌-z }^wVGQ IAR8W%4>`}iEI3]Ӟ=)<т&lcu ɾ.]]@fķ+a'zr9=`kuuK@ =*%5A=f=f^~yWL7 Uh`O:9,w޻X"j|urScA#'NCdZsenmd+J-ci_E?7W/I&"T[a#X#OFDqkRv<wD~WYb}ۄ0-#g>i%pD<+LH?b%Q{|߫124$0 R-3YZLvx4: RnKy K8h.2wͲ>0#tS{y!{+߾͘),`rL&~9Za VLbx{B'',Ӄz3Kӡ9 ]Cs<;(O8{U}%Z_U+ne\)cs7i }ߣAAn܃HUt١} sAfnveT\֢|샳a ,;遃Ӽ t\gUd>Ł5`X]=¼H4V1E]Ęn% ݠ!XW*b17HvY%FHzU+iLB^ #,'Z∛^6]Db{? Ղ`,!j" Ϻw)wg{ R0@ルMZW<4V,Gr#.UR0m*Lbuh8ݪ`E>C_S#*_V1? l-LV`9lY/ .rt)Qti;ě|K@^$lNs5CDbd,5K{0wDrrۅ5Vp*vӗeVn[08.w/;alB?Ft>Kk8eau2Ryد; QJͯq+|ߘl9ZphlY++2 d%2F@_os[UzR󍥭x6`||Hr2YrDqdLbj ΉG$bQrO;ңd ;dpx*U ?B^PSr(h7=< ]{-3m|1O89yŰ%qgYt2QBV *9&SoiCYhb"c&(9 Gꌘl@mAczUw(QJ hTln̪{•!}h|^ n~xIrn.nG6!HOŭ7ǑザlEĢ0%r vy :n"m5 2+j$PB%A6QCՐASGӃ${TV%ԏ?5 `rH>ZLʇ!՚.ENnhqG&Gͺ;,䫺8."ۯךxN8Ҥ{HZ7XhᲕ̹*)gQ}r0$R+qb|]n7ʭ-lʔOmA[m-h,{AfFR,pTĞC4=v_vۺQ^Mˋ$$te-;bek,Ǚ6r@oLRϻay+7 T{5Uvwq=paFZFJ0-_k>[hњ,*wӗ:FZ4r*/dױc;Ns8WfT p k9<+f>Yap@|A9(ꛃy~Vf]-nKgufn_W/Ī[*^2;+w\mBgST@AAY _`:`.,dV ٙٱj'j4%QLAX䦣Ζ`:?r~R]7@]u\PE[L,pN2!W:hk42: a>[vx\lksdm˰7N祝Y\KLOYX " I'N ndbq,/)ǫqE;HmZ&4D7/1='=Hf*ZOWcB=86'? }6.M* a!XKV ?K)n5M7`\rgL "Ze&TY|P}!=CrqtFj QbB[1C9Ә ~[ЃI5.5mlry҇ NS+Y}, u3 FN]k|~'ȳU\a Ȍ{_Z$BX(?,{z;oO8ϮE$D^T)ɒ'zTbbJ^l]\ A#ۚ q;J`ҥ b2;JdCV6:'k=s"W,]B#sգ-*}z^0`ܵYWJ3;|1%*NmvIUQc35 v3:kT@R'77b3{nůO]kB>N-'pC]P}ǭ)v}f G&l=i&7EM&Ȫ_U/iƵˁ)B;LD^x%x$9`~ve׺G~A(÷n_=K {(¾p2Qêkc` V:is,i#׺K*؅(غ$Ыpv}.oY$l`zLRru#,1cW={,"b.8!)b2H|1U)Ψ=Oe^b>KBt~ge}$.Dxѽ=BAI/`jTEvT Yt+_F0~e/ ۔\ Dޔa~e?Jѧ\dM 91ƊGc ($>6,C;Z;%0æ0#- m^S] 1ľn>> C=j\=UL[\ .+2 T|z O؛ڶ 0c UBGf >D!SL- I"l՘ BGQS>}+ɏ4[ЪpDy!&M@RBΒŵ06܍LQI D4!5NPڭ?sfQ%7l?̹E;37~1ݔر/rHc;$y1l~~f- Q˴B͖WPw[d.QI1&KGFIǫL5AF-Qx{n'Nb)4\BS+nQFD&ԆI‰+Nc djj|xbOTǕ#Dys;|u>T ]&HWbX?Ygv|oei^)C^u*f;~M{Pr>"?G+x>`GCii||>Ǧ!4T91hmhcG1E(gjuA8OvFlkL7J"tAܟ=H>݀?Ζn%m(pDFO+(@(*btCU:e@-A y9"o q&e#bw"mVjꚶYj$qTmL2YWM~lsX?d 8nB1l;H&V sʤ솑6"v S2-k.̺ưZbZ⊎^ SeRQM_a1 Kd}RCu/soûU*u*6S╟MN[|8IFd-;{Q>LțbČi^Ԛ5= >া-spi!ίvo yf!A։z9Ԥ66Ra?bY0+EN}P2#2Vg"L |G;XW4tI\FPa~/cP8uOIZhiF_|Ŕ4rCX2b.Вm{ j OFx-, .#a,wq$d&k#i[ EH͢l RJGbVy">ewtpUG+עj}TzeV7e()zMwW?oVo[0s_2eK>60ldO [Қ/ȨsX YS/˶FyVUOts+otKeŴ. mvk.3E6UAM[J&mׂIqcΖjKW?eMV48~};- c)tZ1o̘Jס|I=j-kJ|-v389)~=3W`T%8ԓH y {Lg 2ς&n~;`<,N_T ĝvMHC<5MK$: :Bs\sH#̇!yU9ԜͰigy/gg!؋}H7qZK+UDYcC$J]?TJyž҅0^҉}Cy0sqM?d"-)>|>؆E&PƗ:`sPWOv V$Ο>D+g=h"@A{#s׼]e@>Q^ܯH2 F4[/;xmL5ISc*ye/'jl3W1W-O53JPנSH ʵN |@w剄eI>Vp\-ez.-o)U#s{`j^oq7H[vD}~~R,l:5z&] &M"?}NѿHdYܧ3Va~tq7P29&V$%zKj]s ͪqI]ۇ׃pQ@5e9 |b#jK2Ú?]^MZ=<; x?IҨaGTXM05hB3ў7|e1dlNBxMoCartd% Jߓ,h0kBsSYa [:8`'I8", ̪j9YRdW4lc`M LsȡqPQY34 $~.\1$Y݇懺UbzwueʧH@-9~J&l#D?9OZ"~aQVX;G6݋*x C"74^+.AF2C'4PtG% ͸]--5#$ jye6Yn*X :;bup],$_܊!&V7ߤj061'/SaYT5PȻQkdhv4,|gHK@! x7:J*[%or^U e+?wϵ>\ߺ æ.-JR)hB]@yz) &nCFuV"`ŶSbsX{W@jUDxZrv5dE쮁I caVPf׊A65N%1)GzW_[(Dd0ǥkڙ,7q]v/J|޳z mG:SQ7u'ŋI` S7t%mxpcQmTɭfyF3 #:٨g%?]qGۛd>֎g4Ͼyڽ/V[бb')vUL%G@!>((DZj܄g7{䌕*uye#n?}%,vfYpvz.>9?5}gJUfM 0?n{ D G9䄖z 'P6fR@,M!2t'7{ת|o>AԎ`i1=7n6(5^ٰ0@ҍrݘ˚&=Cr6.K4)Nkb%J& 3+M$fZg ^9^W.Sg,Hcˤ}Bd> ɑN_x޼r`}Y~n_P=m&YMxqk5Q q+kj<%o5BY Y w_ٗ %Ao])w 4{zsѲjs=D*~3@JP 5B$)骻]{Uy5@fcڳ#Q b$>.6M8uˍ6:xq`B'챗Fbe#C1*C:fⵯ[Myjw|:{8O%ggU`4G+rmɛ 1Say 6]pUHU&GP\Cn=%!Ԇh۽|bfIОnoCDg<'4baO2Ju ocDxfP7V\|4VaUՀi'/Wb]"5U./n$7m &™aYSb-.#jBۭM(j~ -˻F\:'N;r_0|WeaM ' B\ǟ| )^#`Ը1zZVm݂~LFl㺒O5v&٦9nzHH,|8ϖQv "PO hz}Zjb&e7Aʖjl4n;3ފHj/v#tY%y36tB<3e\jhoNT~ U?л] mάvMQɳv2gJI@2/,ݣѩ>IǠ/%>THہ8XcrylkQ/֑QENJ )KIiE-g8#.!+bI$+F`8vϯqRw 纎 awͲaY|(?0e ## =SQuޡPvlYG@VrG?85 R)/j1=eǡB4H̎U-c@׮QR JSR{d-zy,A-OÊ?Yh6c|v*UWQ d>RAAUudhzQ6lu!v1ЉݡQLzZ[aHj:[R.g!o<êVC| c%4b5ubW!)!_j fÕm"K  v H?;YZ /_Win{ <_+vH8O&0ia7߉Nbzo3npaD0r70[0sضBpKoT"ikG]xb N1V%6>{lEoK<7˚-IM-Yhm< Gek~G{7ʼn2(W(guO!WY$njr-KP:>ۣ<4ġ 1vi)\>fŁVqPkCj€qM` W oeX'QdʠfABV]&]=0ZUn*9~0?N;i_?E aocۆ+r,vEvc"N?)mKK |'A 66$< /sPk2PL5 vN*HX&\)N}vf:FU1*֚4FwzLø*;z>8 )q(~녬&,nL?-9T=`[9F-ԈRKx{N ᥝ%ruQO@=0k x"X1 /EXy`>I[;o7V/|9$q)-ɀ=נ݈9]Qx0ZHk3 -;}T\doP-JMaRWLES/ 3vmTc j 9߲k.lT|]"Qmty(/Yvbnm߲ Cރ:CZصi [m )x?Vq2$a+OocB{>(Y47b)5@4&r-{svOv6Tmdϊ\(-/q$,L>^fm=hrUͨkҳr ?f[aK]t־ x\Ae&*2i=ZE,4h1B j*B)ODbJވej}Z n-d ![g1vtf\ ׇ64| .:ЮbV=+" z W:Rmˍi$^$&+KԌ$P2!ȏ־oR2YQll)m*& =X^2q1T~FCHi)0;7'&v"F yjܷ4O.s]hI 4]P.N>ųC s! 9m]}=+ń-.TֽӮկf "Wg{t07-`*#ɊhŮ(AswUʶp46&:~NZҾo؉Y30N4*'<) Г2#.qn?*_nRVw(FjsWUv6 [,7{]:|_(Ut?`-AQ2P \8p@$,_&EB^2[Y^2(0:-_`^3:lSW7[PeB¼iI&JEF.Ij0gԹNHBE;!Rg^w^i)b2[Ո jWD5δR ס.e7`@.dybaeJstW"8cr͔^P0ˊ,Sm%.GjOU#XD Ǝ?( _jɾiƯpe';ӍEC ߵ;F aۣ K0A 1ֻ^a[v.@4S*؀r>. {{jC]aߡLvnXđMk\* 4ڊ r/Us ,7@`ʌLJ_$:I=L`:3Ja`-Z% -g ÐHgR[VuFNd n1T4H.f Պpd8D2Qi$XMr-.ĝ$2Lfb enRtَO+#Gnk @ٳ![`#6RorɌBL/O&\PvBg84.Mfͫ,[0Cx I|BQkX͢V\_c0sݑ>p|0E 4,.nͶ^ t8:FCc!s\or*S&2Tą*it O<ADJ aj vmR+ڠ4Gc0 KCnkC͞zQ!dF=qG!Щ_PFi)(e u߂ -jgL-){eb8RJB+^":!qZ>XxWt ߱eTКÍ|rm,tEUЦhuBDI) -.p`~8Wm/=z& p_L5 T-&2K4*4WYb&i$Us`9R8l-o6֧"oXBhYNg>QYU$`Cews.qȢ!;iz)墮*mK5Q3D5~ Ѽ?Ne+ƾd *.@I}ORA ڴ] 0Z9問!ďs߲C,vբ4\>Y|x.3l#4D{QcZ:mB Df`C%#"T=M%6G}d fQ քCn5b#h5.}[kcwŎxRGQO;o9yȬyK8N%}Br ]p@Ca{j}+Z5v@; WS&HAv>prjV; )QAJ\uKqTɁ"PTտD[.uS߬((&c xwn*c~h˹\sq~G9FVuj&P:{'=ϕwxm-CBJ OҬECL3]g"/S5!z;mCNUInDzb s\ظ<}K)7ׄgq9#G+ P!nڂV ̺rrA";c\!PJxE[ALjuaS8V FY\C\dWCvJa Sd/Пɩ6 =IKҝ=ϧ[~N%̴cTYbNnf+]n]z55k>A|ܘuk;Bthfݞ'E)G-G9"d,@xKr~X ~E m6eTW_p!Ec `F$>idXїtl_ҝ72I!LCA#l+ Y@;LSNJw%gTkԌiBZ NQW~lù~h0Nj'B1jퟷYBi%՛RVdb R%9Vۖ(>*R 2:%:ӥOηmmYv?y@>y=U4w IHfu;<]A8"VuMH4^ONmcC/0͸XMJYc]=[S /Lj>WZs WrxY_w"}z=WaNh0ys1 i $'Z73'kM~m:.~IAv/ћRm8Z$ k*pjWeLƇC\=pJf4P/@O-ԄTM-@-nXt7m:`e4 Q1ecN BD O&ҥ~!m|+$Yfw>#oZVZԩ-ͫ!2=VW\ʘN9twg%Y=ƱqvO ,v,d< {"i9VU޴qW~[RKJ%b=1B[uJ掋ktN% g ZuZpWZӸK1W5ШۦNhCy?C>"M(d6!> fU3:s(gSK$wAC//U籅}kOmhŪ?T󖌡ri.ʠqKu΋XsL ;d VVp2ٞ L?B>-aȋ vbg}|r@hE\l@M#ʧc EH Ƅ;V1ڰG@G].f5FTy" b@e `2/5*O!CR=cV{w x bv!~.؜'N,ټS#uuy NpUD H6)*zJ3;Xh'PnYX8i{&$+MUr(5#lXӣjMijC,h\HAA@MHυ⛵|?"X>U>f3X )P7j_G `ʥSmFq"ߩ5 aCPG R@҉+eY0tn$ݘc5 )o/p'D._q68N|,ix+_<\Oac_ P|1xC^8zM+L$}Fl&WKŞfjWW{M7QՌ81f:/V_Ϭ\@-h~X 1e'1ZW&^yG+nSUÖMye8֥kt WT[1 ݱ 4`w񦤣D +h W@T $8ԉ$RC\|H>Sed q<^VcTa%w"`[s2UOo"sl;;+1%;8ڲ@/^V6%Fг8$%LQWYxfہvB_,Ǜq]Lǡ́feMq? })f, >W4"u'ըݼmaoLu;Öc;KTrU:VxEWڔ(먰 qKʅ趱Ax?rG-Ĥ6$,Fǔ ;* a!Ě' {#3ُl"5z V/p7֧:+M;>:]HϢ.:7\pw֩%5 +;bFy丄pI‮۽?~m`!J[9O-ng}L٫43udo#iԖ8VC0ӁPole+R2gD 3x2Q!PypB85%z 4j4[CU;ffd* J~J:̅] ~O~i9p_f\G38o2g l&#wRLcQsGhSjXNӗz[,Z4OS}M\ <:i/+tf)4轃V nM|[oKʖ'3x Y9Yz:FO:P,r (Y!B[ V AMΣ_ёᜧW\1^VZ*es6x lZh~s,ËXcmѸ _̀(Kseir#:_kjD-6X l(o naNr*M9hlL` W?;=" $ANh20?ȐHC cZuS =Ƣ{S'>)zP>C숈bcYS, *mB(b!ؾ\Og', 6c@:t Tnch#Ylscګz&.S#ӷLK@7dab~2R)MR[t6[ :T0>j, #-w<=TP mv>sOlDmn)ϏU^qO/S3<H22]1~at69 N΀.cO׊-QyAl"f{lC/t=I^9 ._F T\XYXMB?g ̝ l"޴9tZFzzn) 3'$ 4"!<"_$kj  myWM1AZLt<}'h wL ^˙|2C(IWAB''wKC\ Nĩ8*a ʐy@nvظ{mN|>/m! *YށL qK:} endstream endobj 935 0 obj << /Length1 1675 /Length2 6865 /Length3 0 /Length 7805 /Filter /FlateDecode >> stream x}uTk)I$sh$a`d!ABCiAN~ssy~kk?^w}]׺YzrVK -H  ' E@H__WBHD 5]A6|BQqq1>XBlp?pk_︕\!N(;ʔBa+5j N0E5!VP]UE`P:+A!V@(l @:@ N0(D8C4?kP gI QS$F.2Pأ8 '[oP Jj::9ǿ`n@|~ PT;Z^8ppANID+'7xoH 7$UDP?W*7$U! 5nyC(wBkKb(w towCB^BPF7r7!IJi/ -@` b'w3a;g*$%浺(ȿ(z-j@YBQ6j{2[[BT_BTn!GT?uTɷG nY DBT7P-G?*[R1B 9q Q @v3.'-&[*e@~ Q%xBT   G{8;LoC:! FP+m&uMxP?Ow#ܽ~aTbb>%89_5Pԅ@!`d29:7_8_â #Y{j(8[XS&C{qn 5yDSl_>o0X!hmEIwbtZb}x)"R}yD|`="a|KK)ydS3H2'$}UK`QKUA &_TU|sLp\ }~tGe uٲ65R>U Iv!e آK ƴZQ鉩zcǓƀ{#4<,HLFX\ףz9X ݄DzSRU4LFgrfL~@ y!kbcYzϪ{ |(頀5"}H}p@{GxQ" ȉ+_b;GZF{]m$iɰS:NZ\O_ f|H~ NFLx@!9 uzkk-hhI>1dݴ<tM} B61X…2[ 23-`D.`xE(9ڻtUSF%iNjIX yP7]IAI։AlV]Lb/U|v&3~KoN( Դ;-GﮒM:wZ]Uc6#]6_4{<*a N,kpymF$[jE;z{#htrxգ8MaSqGx* D)ZsБlZ $^CŰUpIk3qH_E'ΛKǵ鎂?gM]EDn-]Ma-+ه%y{?D`@>N Fl9:=otשg65E-}Y,(=msv<%{]uBa{.n.[5՜.Y-Y=4I [)0*4LT;ݯZܴ<6Х5Ebr..*ۤ0 ٝjr*.:i禓Ӗs|^v, 9y.6E㜓p껦qnT/QIW˳oΚ9}l|!>l}t[[PSx/)-IUQ'7/g@߁PYD^vdϹY.OD[% &E=<#fOTc9l~?N-NnFBl1]uxB1{O <7NWp ?Lh*ƊsԅfzןB/Iw|5"fJtW$6BB3K|N/C9i? ^1yjܮi^lۦuEٖ4)}q 3]a^ž[ە˷cX~>`Y4* "/;i?9wH'ؔ9w'ԼɅ30 O`ܳq܃A&4c }x1]tw(JvdP)\J6mx,uRe9;s_Sj b\dFH!l ?#fvW>2DKkiWTNrru,FwR5$tkaZnȪ쏹_hyD\]@GudԈϊe"Ž]';|>ES'zOdi/[HR\'ŸSc i<&="թ+>T'MSAݯ//]u+ەш..LN_Y{GIe\)NZuOj/%CiQr]W*a<qzQZ޸iѫ6•TQކAn۞m)%asҒr_mvHd"ܒ S^zC 8TSi&cØۧl4@Fa*8_~WF DL4ҶuSQ4|T;9A<%kx^*$$@)3|C7IQF{cT&kgz[?Fzd9uVN HTU즰Fd ϳ{ S+u-.a~g'yD)LX vb5$1Aߎ:_d$H<p};j/*^η %D㿎ﴃ>9p^Ix$tx9@wv [gm7ևz— Y,g9\ @+I=(5竫!TUhiI?23e?e,03~&29U|`V-1CHl7Y&^dr~G -EBV>R@Ғ*1:~ihwO0ik:eqt.:*2?Si5U.Ğe:k`vҀ3AMsS!0K0%l$8x:IZqqGŏj.ON fĻLSD.9dnpC`+򣧢9vFGT^ŗ RM6U:Sm{\NQtkP fǘ:b;777 |Zv$)-h>Ŵ(&y:\| 9\~8$7o vfYl(Dd^euQK*4tdꄷ~?3Z_E?X')&S^mUr^'g63 8X䂨3x)\`*iIw{CLumL[ׯȚh|hf܍pmt쥏OcxrĪМ4E ]`j֋NLca]XM.ɩUָK]LXh!]7P2^EP2dt7>o;r8U-РO!V e',biD/$f e=5N5D^v{I׽6 u"gRSNu}y|\ n˰"'o_3kwZ7;Ы>Oz)Rw? =^Ӡ6 oR%T#T_MfGReeٲEj LpՎThz+ܑ.Q =DR<IJR+j°Vi"ltP^x6R/xk"Է.~b)Nm=+G YedXeTuft(!0(}}I8 eĿLBrDOi\yDM2汯=aqBps&[`h`^6o ՙIKxuI0 Atxi;q{XHX*s9FzM~}(%!$r).F2Bߕ{{S.aؾyț,Nk0fO 37ө xEe޿⃈^W?gCC>޷~9.m)uh->Q89}ZҀ;tO"7&ܙFO3o&.±}@{*~ëh*OZD1!t.1, &[˟˛2$_5ށj5:U 9$8g1.gYAeM(&Tvf_H#vml-ܥ~1#⵹dAc}<,3iS7Qp`9fG`b`V"Z5ҥwgrR@)YDk$6bR6Wr<.@K(5OF K gFwFiւie?Wvrœp/i+84K j-Ⱦ[=2)s6~?"Q[x("cB V@cqQXwmvUKv/CS w{4t_U"$C'51dQ>zՏTq|`JM6 endstream endobj 937 0 obj << /Length1 2238 /Length2 9797 /Length3 0 /Length 11013 /Filter /FlateDecode >> stream x}wuXj6 ]K#4ݝ Jw tHtHԷg{fefq9C`@ ppa00HCA03Dzrsy908z wSuw ؀a+;"qwu'0Ó>S8C6 [:wGG5K'/]lv8v0*VE#ZbpM^  0u!6 #pvn ?6{IkO *+YY 0 o?bmw&pJ=Ŀlkg0B-1@8`v/ ^,'_pqlg8S 8eS${@@p*< p*= עZT\kQ µh< xvϮu<g{@ٍ|?W,#%`k07AnVbeihf R8 m9la|@ xA|pΎO%'N>[-7O\ppп[:>DtGK?;da #oi.vIvߗ$Ox97΁p/M~xc_1vxwwxH_MM;YA4<@x.fxK({

|tlXg^CX>xin ' oCq{=胗?q>/f{my:Xs~;ܬ>pSw?xqZټp9@xb)|@п-ѰC`:.0o :;60?]T-˘ ~2~>VRR^|G~~~v·  A /5pO=7GPV# ?gs}UP'],l*HR5JC'f^>e92|C!u̞7pueq&H?JФĵ0F6fJҟ ٪^pe$pģIy nC2_ @ߺ(" *&?4 iuf'#\A~(}z7ޕ&PF9FyaHeʫ(j/eim8PUi1uV!I[X ZО^զ,CbU]+v.xM9$ujD ,ZZ@uJr,ONWF4jTR1I]:/忐 g7;=q,Ho=7HZaxb! ;㛿}}3b#qdRIX>/0=q)ݳ+=_8WX'XQ t%ڃU6U/Ꙭ%A}D3rv{3NuKMWkjA#TM;oCRڊIQZ0KyfM+d>X֬LϤxDtTg.eNH}*ޮ(٬h-$hT k-'̴Hs0HOWȤ2 %f#l#uG`-O~c!m|_ 335 IYݵZ.YR辬;Q -OȌvy^4MYJ1Zutd%tʸAulgw& ?Wq4ԄC,3~Jd:UK']_WfTp"Ɉo-9ljF+9eK#2NGXknE!O"1oよt叨걼Ȍo1'!!~AoͶ,Ҳ\spJ $@v+TXټu]7&)GW0w @HM@>Jg8W{4suYMsQ\iƗడz5ϔ^NgA=oq`VsX'GM.Cq5&S"ɽi==U .Xm{t0Ux/ .h[lL1ҧ!T\ܼ/8$h~,1q>,EAE0{Ӟ[gVY2B,l{8Zg)9d]s2%/yvاGӞU5VUpV 2Bnԑ1s;l$X_j;ϣeZ,dV"3B#ķ DF`~|U3gW䱄!@k<"ԥ6ڷK X5~=W&j8R~V͟YߞDY߰(a_DLUPypWsV`35~ N?  #04>'D$νk::yK2QS c0הU0AkNi^#>.&:_hhy iE;yԒYM6ZgjQRrMrJ$-nGIg\Ij2SUaQfӾ^Jeݮ8da ()U'/v,t.$,lzajRWh;k_ 1@M!yDg6!mʄ:%[|9|'',w! ^@v`qfz T4MdD|v h1/ds=r򋽃P@t;.Fy!1W=61rxE#!A #A)3V<ںQiMl,V "&x)E~ "̍Ֆᕮ?p; _Sb4N\e}IXY,}%0[Jhs@<fW_MC)˝WM"ĐMոc^{7_Pu27歕A`{#!Q|{2ED_ |u[rj NO7g1^1@Nnkl3Ãa-Wm@V>w)j3OYrDB7+>RǐKB5*&!6 ڔ)r2c[aU1 HBB؋>{ .=̱{.`"YB]KR7>hK64ݦ'rYˁ8γB+\CWJVJozu) =|Oڦ"GLDqȢw||GvM]4^-ME 6IF}qxs9Tub5fF;u78vEp H~S|po+S|(,LZ뚪Hn]р{9m2k߶3X 3Q=}{:t Ef{l֦}#;o6_qvǍe:ӫ:5mI_?jCoP9 jsHt_YXo y 3=z)[}>):п:ȕV kzn~rq{e5J+D=§DCSm:#,Z2|v"**f[R؈C"DMVlUY>k3|Ea` K~5I4VZy]-<'k5"PDڬVGaL6PYrD# sdw]B<C4e-2*9jQO!8aYJ=Nj0鱿?u-VyE#醼QA~SJ"áͫ U`-Ykiu.7Ǣ|AMd 4c*F ᥽- ;<6[/EԤ\'i33\GfƵƒf_Ts[Efx 6Ob&" x|F9qEpr?BrVV ʖ\`ܺ#H#V@+*"*{TDR9AId]C U-2U [o=v!i&Qm,{Xg%Piӛ%Sr3bӄC/\ã+ 7O K qQTm-+l?Ai?P٩ Ȓr}ÊO)s6T@#Zo/R0tK<Ĕ'md 颐4w}^SթLS^.N"!T'&cܹM^1 ji#;K0%0pŘ O܌|٬3:<(S~C]~TR}Zb*|~. $/L S"ؐ_ 4OgoSm7hRd?↗H/1MNl-ώ7SnN5ؑX&V#G`K2\ME_e(GsρeBmo6'QfeP3ZAvاWR]X"p[2O "QziwZwQje )JϱӞ}6&B]ڄWp=41Y r[twmhOs6!OLJ`RϘ:vo uX>]dTy;1~;lS_9$1罵69;'rK\:G;ҹw},cٺa\SWP4HАi?QcArA@>!;p;MTGNDuiQv8g gqkIGka K|e<{S&iK kmƏ.\yQ;JX8r/q$O=ۯ}ؤݶDox7o^kʏJ6;IkmZNfdʔr̭"Rʄ\yFd30FTvM4qڔEJdhN:G52YiQL}kK[X枉|t2ǰȢ2xoi)B_R=3#{ōI 3ZiF]f}\ӨKG4Hhi1C54gT־nP@󍩣(GMI_ԂQV^'`<=5Մ<¶_/L,25~2=A>)"krxE2`kD?3r |+#agW-<5[?TuJhqD\SCFJ$TJ m:2Rq_﯐5l'&G*XGOQI{$eG1<nA:Rjw=3SH+x7`"~tԞ({r{>ߕ,^BpEF#/iT#HRc!F. !gb2%}R+"tDM!afscUq=Vr]PmMR޾R Yo H+=l Ssbecw4Nf+am/cޖ'c\{e/|xcX+F:|ߗ*j*-hCrc%jҙ2c;70re#\UY_Qc;d4Y}Ô^tzv^kRF½_vhjS//QJf e`eQB285lU4_O4]Ӱ{kOmk4u^HGڠ_il_У|߂hW@GQ(D>yFdY߰<<3x''`ognw*ԦUN.ѮhRzp=C9i`n[sq5=>3F*<&Ͼsq]K`"'CG#}Z O\XTgyUJ @]IQoh)u72w ]L񒚕7Pcx8ܵYգ*gU6s~Q?xRmTrfp0;&z 1q@OlOǝ1&@sÅ0^/ x﮸.~}Pݑ $0ȲqR%ق7 ֯ b1!-ڇ7Յ n/yzEc,#=>VgY\2WE+Wv$fm$c;Je2+0CyН:|F wFK u3ނ>yy5a 6[zI?%>Mi$ABeT\pպh`-aNG03 eI~T͑?ѡf~ 2-؆Lmi8J%~itOhMw-MĄg%PQ>t 排:"@^]|Drψ ǟKk͸~˸̍rT`Kݦ3AB\"UU7 yBa 1AacđXOw7_aQEb>eD!Ŷm`j1ހ*^;g\Q2A9g$[ҝ`fiu)m 2aӞΑ3ȗ`6oc H+quߤ+㝉__u;)i IL5J-ŁXM%5D}0S#n(oGIfpgq(v)a lqo+³4n7kwBk:ih;#jFD;mPSz* H'8Y;עjM$&Es)gA@;6-;$|^WpZ_(WUc6^$%ddOFho4y(2u?]ʓlJ+&Y&bM*"o.fsH ?اry!-v΄_!6Έts똇1:fر@d=ƣBh&m9gJu@S!źaqi="4F%|yksd1h{jK,;8Um`Un)O>fe[zaQ:ΠL, CP_mgCԦ|UB/ GR_lRTHd4<ηWB'x;奲TdULv]Ld#Tg/ܦnO}uo78OFu'kҬidQhxDzCH4w>>@]@<>c6ꧤѼ$3il5CcHe1īɽ(M7 Qo{`,6YκEeqֆz "/y lY;::3ŧBe k/!s-g-Ww\b3Mj 2l0ĝ/?7XUlAwސN=|KI+Ls7Y+C@uC`K'wPݝ ! +"ow2'm5`~8)7A֌@9'~?I*cXC`eg`yo1v3BL=QL (I!t9N`k'K6-/l;Ծx~O6)tF`<]!!.-9: 3'FfGWx>+Dz5 =^As k <>bNaq']d{}"R|Qh| L~OX^{wbV{C 5Z]]TRDz#hnp%mq/d5(K8OΦ@E"{/nR⫃2"$6죠Ǻ4J% w&ƄȞqk{Y1*YUpD12LլB잊ܔ.ߩuٶOWlY54Ȫ~ؤ.J``UǕm写oyK͠J"n&mj6ۯ~8t&=b֖ri<> &FThҖJEypH!H^D?:}7z4>80:@ȸ w=(?2䘵ǥJZ!Tu mbV';Y5L\9x p4rJA+)hk,)XQ?3:RFk'A@"Ц%u^  ^49MkނE%e;5ӻ .Bg[Zξ}ܩN*7LL"Áxј3.xGQ! endstream endobj 939 0 obj << /Length1 1898 /Length2 7772 /Length3 0 /Length 8834 /Filter /FlateDecode >> stream x}uXm锐FTإSCra\jY)%iCw~~s3s,0khsHB,rN.N0@x8A8LLp(a(F@\@.^ 7HF~PE驺Gy8 Z:Wtrpm9"EHQ6R@Vsn0 `;¼?P[kU us_" tAB rP-A6:Bp#T`\!ۧcckiuuǥ愰DVF}>ko9Pj uDrg_Іf\G:Z:Al|0"@=m9#g7 M?(uG 2w$C P@;T#!TvGH=HqGH=;BiRO玐ROzw3;BFZC\HA 8 Z!0 so=x!sY?ćԶt!o--w@?ȃ2 @VKK ˂y``? o"CR ֿO]d6w#Gdlu#ia"qW"?mΏr!xJnb?*BtW#2#7/;723uߏٌ3>8AH] 2PF'b du΋+ jGR."?A?]RRN>|U\|@AnnYr/l+P'grR$Ю"$J?Bs[ÎP>,T)sHE_DQ/5ـqkګDLlǒVk#@Rn&-KU@ڇf׵BԈHɌ_Fn:ר׊½&AN_^P̡dozZsm$:_T)xIv˷K$ CkXS<-jps,Dnȋ3yVM 4>QO:Ҍ=>%Hel܋J^asmNb +xbDjꭼ|~Z~gb$%oPYWT݇YoM8ˡ9^t8cU[ЈZ^cص9] ZddTL6_"PpoBJ8d+cvwy6JʣY(ׅ(/JzDʽ8w eڕtyB ̈́! 9OJŹ\b|AT|FZ /v~p( 岪.4L! (RL('|cڛ7I2(z4w5Pgac˫ur:/=LѨ/vk<!?$ _D{ޒsD4*CEkq][U^ Ѷd[;*o, `"} %>ʧe𔈱pUx8?PY4gX|'^=xO=os$){$7MD~<.w5;h\?* eR"J 4/ڟٶv9\ 1gH(621`&գ hz ZqGQ-gu{|uYBJT㐺wZł|!I1an) J2>eq, )u+ͪ0tl+ aG?~NmD~؜LW9c\c ޵թJq CͲ&A/'oCw96p}jww.<1CPq ΉѨXm aKF욎.S>S.k#a5x.XS/sqAMa.rk+ ؔcH#9X^b/4 (Ja|W\=B&QiUx9l5xx#APHְcĔ؛|#h ID mfN] //FKqH*]aoE3kCJ|zaCOzQR׵VPG9J9Æj% @ rֿtS.$HEYfhdU%ɯ6mnng{zU<.b=ݬ] uTP3 kZ [  Չ6< Js1d59iˀڴѴiꑽ"_JfdR83V49 ݺXJ܎~9 K\y]\q83}[wmt15>Х]@JV0(!nSE/KjO%_Qaw /spFc?߬{a[ vBz6Vsy@T;歯+ats6D6KZ8̿^< Y m-0B={#k~%,LW;C_dO~;-=#gր0𾏤A}iC߀K*(;ŕI۱m9Օ'4'C>v9y62ƹ1}N.Wb]U=z%|y ri/ T{}~;왎aYD>/"!LyF^N]զ%s:wNۯ%H%uo+:XZg\31Wbz)<[b,JD juCߢ Zh[`bt[=|RbrlGg#D ?13 >O1BX%hx=~~IgL ]6=x%xo5^re#PXF/q5.Htqkp%&ҪH }\èluG:ׁkp?L,>o %O |5ÉM/ͥGT{5 aܻ]؍xP/̄?AKG/Í;̔%:=:?`m0` Rj6`ޗ/;Ke4BV4o[HhL B5ݹn@y `x8.Rsף'^%[΍Z. rXZ(9Y?Ppb>8 | {ҢQDw7UѦOTk%Zw,?kJ > ۘ? o$ȇƚ9m12Ǿ{cZΦwua Us~uh.۟l$A"x.HYO+UO+5Ŧ̲%#'_vZ: <,q6r_#"&3ˈ "=R*}2ȡԤư&U4퀊Sl"e+^d\}<:Qzxo,8w2J7ga9 deȨn,ǐ *{rbMU-RD.ŹG3d,) Szr28 ~jKi&?'n5lx.^}YhRm](Oj>=*#M-+79YDܽ](6ۢ+/~|4i/ u\|>8||UE5ީVN}FaYk"a"aK~N~4MfhOfa =kԉ[Re3KW ˬ+#.sF_'d5Eϻh hTBKS7Ԣ oe͛jn] 'jrlSc^> T'ohd|G? g3ybPxcg7 9O\+΁땪*j:N21uv\ (o;mdkHqVW=3O,,W\uML+F2('d't+nvۓlJ Ķm 6 ~^e+2!]dR*f0>^y(6`OhEE1)M1a%i䘒:κ_ D` &Kbp+ R}c'f-A&-KӲ#faOi"ŏ#m-{'^?us_P/^E]\I0ɢ4Ink+XۖXNuqwXq>-\ ˫|#.VLa~_M>ښu^i>>XAv?]dD\,]|^۠1R70 "{A9z%Z 9vfƤ7㚶W915CGrv:57}cՔJ>G%j jHx7hF?{7/n*v(@ǡ84|3,-q) W638H󿵄O6x?L܎MoF7` ]d>GSI.&D{]#蒢^ e^XVGrefi[*S\\~ql0+~95Z(?%m S }.D]&Ȁp8YS1fq~3h\j 8_\&4*˔\%soNB3_?©%?>l=!%ɏj {]n^++;B_$Q@i"H} :(ɪo J5 ;ojZYUɘWBic6 EOTc)t^_iЏ`O:6fƘ!ia4Ӫnf9U,PP wۭi5 |o-Tf=op.e! KZ߻[ ?A7u> stream xSU uLOJu+53R(I()Q03RUu.JM,sI,IR04747200PH,򬀌_0t ##cC#c.#<.}yi f\YjQ1 @+Sr*RRӀJsrsS4N̩ģ <53=DA75%4]ֳ$1'31/='U"YYYPRT KI-K /\HFfrv^jq)T/$30>QpAwHj.0`%'(X)#@j^ z0CN>^ڐH˹%d+)$%Vr(y Ն @W(V=_ԢPPZR_#cKLp0%P-H+ ,!5`!\RR7("@(0V,+uM-na + 3`ע(L.-*8yO:;5"5d떬M-{~lp.}z}"v]_P>RSBZnlxFZݏs14ޛ1c O|0=U.%_*ǂ''DDOuϖWUm\mIߖ{^5gxuzN~.;sAʒ~8vZS-BE9o=r\9=Ǻzy. S4a{҇o=Z9ֱ$8|?42_\>+Qaece7 )f Y[X3_J|NuX?ꅛ'&Om)j^xW=÷v3<6*9bݵ+GVOL6dc ?pN.G&-_WGRwHPW9Ǐ~|͜m]pC;V_eie8e>GμR\#`!_㷪x{{o(Iv|c\O7| Wou4(kNY_{'q?gz;MV֧~Z3&K_iEllJfm䷛YFl{T^q$1k}S張O?z!ҶՓn͟t'Ӏ:PW endstream endobj 943 0 obj << /Length1 1644 /Length2 1359 /Length3 0 /Length 2148 /Filter /FlateDecode >> stream xڭTy\6((ELE k ! &3y $*PTAPQWA(+KV}?;s012g_T-%q/0Pvk ##W (A$bx >,6a\q\ bI`3>YW@ByO<""#) E@ *Djn0 H !J(pP>)%@8v|QEi9LbRn#J?@ @IRXe %8uCDa'H/A$rVIB"6R0c0Η*JRb Dd"V4` !9"KPeR20DI`!B ŭΧ:b\+o}% DcbI*4*f 9fX<"Q6D13TcB98I&Oe'gYg+B/$`lj@ ŢB?@"T(+yXAEBT[1% mned(!0% R}Sڃ0Q U0XL8,0BXALYRgNHЪU?/YF(  2P.FVǃ `XرHƶJ?DOU)Ae id(RŗGqX1H$}4(`T"$Wg+@׸׊s~t RϚذU|&4KO>fVw#<^[.yq`^R ǛF^\0f#ͿqR2[Ҕ/ 3{sfY;af~ܟ.}õ_U/?ңQ7r$EHGUmw*r*]h8p{gZө5:'EZlf 6zGV}fӳjGmltڌV|2Q`'ϫw RwL;ͶVU+gd8U4t(+`Ke4,G'2XO2Sh\CGWt/Uv9wv=+Oe_:rH'M˳CT>ӫ׻#ԅ_nizˊ5O"i|7>پ%IVGf܉>y,<爇λm0 6_t Tٶ3ZUn D-0!BXwTrۺ9M]S NVu<8Hܣ!>="?~7mNCq{sZekWM lrw[館8^sSo2|6=`!ג=q[4ES*"5e_J4Mޕ/ssRSK|Jt%u{ $0mՕ,s_x9̯ ds;XGkj/gkkݞ>Zܖv댖vwlf$w z_vz]6bYS|cӖ'/8+T1 EdoXI"C*'>10}C/ ]' ܱd'%Ms-X6qJ/<5fWOXfoK읨u<:m7+{-hT emW ;Lg],cM;G3qa;Wmn9iZm4iq7mwgw O ߥHiv;쉶Fb6/ '%w6_֨tDaѥ l9[߲z6JQ rO n.9PZ,rҥoۋ78ssh7M6JS͋ jFQMͻRtmkykPc,/~ !^>OXxk|$c:/RetD?)F 9c:ŤOơsyb*)w8Q endstream endobj 945 0 obj << /Length 667 /Filter /FlateDecode >> stream xmTn0C6U@"mT&d@Hۿ_i=3ތo=&Y$ŖfwMіiGc*S_lrgvomSw.x۔oʌQZS|@ݾ;m'N8"_E|îF?y`suShX7 6"!YUCXѻ<1 ymmXE'[[7'v{K׽x`K$b5yڽLGHv"fO(G9)*D+HcuBA B;"C,jdkkjn"cW寽u= Bmu0&tg>ms X{6gX endstream endobj 946 0 obj << /Length 690 /Filter /FlateDecode >> stream xmTMk0WhF.! rض4ekb+]Cbہ_{NKb1oϻڃR?]psѹUׯÃxrFqnmSw>x۔KQYV7!#n_XRƺIľ|YxN|rP̓PRJOMg0I_'1iҢqzgyf y>mm^8;) O}y_/KםTl6rG_?;1n MUmn_~߼`-Fb[>TE 7:Dnxq h͊"<a"E abn9cO rDDJʯϾ2Ba8c ]R05Y `e1 8blWx6yȕRLʼITqZ%ЬưNcp^NY4{G֊p> stream xmTMO0WxHp(6qPU)·hגl$$=o&ˊCg{~>{0cxyM ֐Ҡogo-!Lr/ǔ&XXo᝵|6(7{)sxW2.E ńթ1DOi:vGdяFy Mz endstream endobj 901 0 obj << /Type /ObjStm /N 100 /First 950 /Length 4113 /Filter /FlateDecode >> stream x\[s۶~ׯd,o33$u.vR'flȒ+i_vq@-y0XXPdRB0jCP[c'}ψps™^ν8/ppH*&\{cBN $1 pfa%4,63Gx @C@d0APDj \r XeL h F0 ( EW+Jՠ/NshHBA@1ǎq X+P)P Cv<EB TK$,-аxIP ]XP \ Cq -AfKp@iB^xg8&!/+ÖkC R@YIl\)hi4p5eqZ kQ 5 (k\"@݃j* oԆ6OgaMÒ`RSHţt0 hrZ4_ 0`hF"}ԳQ"14llF77FC 8Zqs^cG `h8Ji?ѷtÛeO?0pm>/:zP- XC ι2rvɏ?zHьЧ_ףjCfP`CkI  .? 3ޢhҘv3=&PC~nea5aE*5D4@Ժ ,d +R7J_WCRqdj}3!5!X_1![xPMH9[C8(iɿ «@ BK{> !,v045b2oAGI=2LEyB6*B8Y_ހE`䆊& "7TL<%'|2r@Bro!]L&tA)X9OB;ڄ\,u ;,t}3V ozT}]ԉła }j 3^װ(.Kygqp(D ZIp 8 %G0ZM<aѱR🂃6*Ԭ5& m!?H_ rUT9KLb'h #R/aT$  ץ9|h,L*`|vs \zxO(i;ҶrlZp?8#1/DVb%{u)[_n܆|@34G`$z\!#^$A†Rܭ@oXF.ȲT~Vx PAab 0uy;ɥ we/u `{j3NJ@D>] Vf\H֞S:k g̳mDe;K/6I$/M=A_2(,,cLc=gf]~(Bns.Q'j4Q| ito׵Z)Ҩ FSjs<$N_ t %K2`7.sRۥ.^vZ+ XjP*`.([gƲYj-r4(k6/,AcqmoCX+Ӫ&N ~R u9vvA*n2ҶQ2iLmEBZ%)4WֵD˥A[-e F~c =gr6O^γ'/:\-qiӛɤZnфV_&+DsW#XlIXt;WHhE6j-I;qL譮NjBƑ,&h{Bv[j}ųnlGC%"f cp`:[Se <8O47lNi# җ{ &[vԽ/~}%z\v n S.ǤY; kbK n\:qWT5{A7@bV;/}x`iOhkhN-oTx|zjZ`w:2&lŖz<:[15fZ,_voiI_Ƌ)v J鉿[O_{!d4]'w4w;M$NpTMnjgC\ȓO̬܈my1 aV^No3*䝷17.,JCnBw10d1 (lǖlvCaOuMϪ9NsIAHH_f]LJ|"l ?@K,}C Q/[9\+zbr?H%ACfoyEnOy_Gʂd~_GnOϳ6o͛por,ft÷-vDͲY,s<,rè6ͯnCrw/Z;C6kF߅C}_oS{TŷZ[! ٺUm=uRoY.C(yRm.6l =`Y}iYe|4vd8JS(ڦ:G7j瘅w{asHD|VMDLKP˘7T!2`J}Smbm06Ұ*ՉkJ:Q1} >:ө] Rʴr(QNe0OpiM>f X#kh$ uL﵆NvcuZ+ٙVi[#}=uQ6ݷa h>+X{axnDt3c֐v%mMZD0[JIW&٨IٸnkdW6>"6Ikr}b.l!KFy#x`!ezq~~E4K^,DHSd5)ukWK1i۱l33yr o[YNS]RJu'H3 t)Hy%|^i\f=I*y_eKCH+zv$%z5[f%'ޢkZ9릕3D/o>nPEKdZZ(hmsVt/-٠Żh-GVEմd-gVd/RmmJWV6huޮto{uo]+ݛ^ݛmJflӥmҶ}qEF\)+6cbwk*J^%+48. e{i5pX?G endstream endobj 996 0 obj << /Producer (pdfTeX-1.40.22) /Author(\376\377\0002\0000\0002\0004\000,\000\040\000J\000U\000B\000E\000\040\000D\000e\000v\000e\000l\000o\000p\000e\000r\000\040\000T\000e\000a\000m\000,\000,\000\040\000F\000o\000r\000s\000c\000h\000u\000n\000g\000s\000z\000e\000n\000t\000r\000u\000m\000\040\000J\000\374\000l\000i\000c\000h\000\040\000G\000m\000b\000H)/Title(\376\377\000J\000U\000B\000E\000\040\000D\000o\000c\000u\000m\000e\000n\000t\000a\000t\000i\000o\000n)/Subject()/Creator(LaTeX with hyperref)/Keywords() /CreationDate (D:20240405142026+02'00') /ModDate (D:20240405142026+02'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.141592653-2.6-1.40.22 (TeX Live 2022/dev/Debian) kpathsea version 6.3.4/dev) >> endobj 953 0 obj << /Type /ObjStm /N 78 /First 659 /Length 2351 /Filter /FlateDecode >> stream xڍZm >Up \|.t78C޺cXJzHRAieQqLruڤeeYn۠r5Ǡ!ȍSw*3:|7*}&ŢJ,7BT ,dI89q,1w0*8D/Z䣉,DD`RJb0,\%-qNV$ʤ"J|LJ+HHp`| wr-V]nE\oȪh<USX#t$g-ݱDa ԉitF씬d!,D DH FCLX.;/1x,V%p.eiIKdrrKfω1. O1-M>j uC8hJOJb> >n:18ZG9Yniç}>{1"61+!r #ֆ}jF}@}@6 ]3ur'b)`uo]j^(D 1 0x^g6h CRwA(e ]uqA*>ͧN}#t#L֘'A<M(=b3o{!fnӽ 7VВtZy階H^uH.lOuA0@DAi 9>! 6<.w"jh6zqnpZB}\@x~@|iFC>\¿\M^~Yz\cRr׮ۦb_.'&/?*Xy Q|].I|.7SJqryBR\rH9o:_\<)~^Kq??_?oolow/?f JO endstream endobj 997 0 obj << /Type /XRef /Index [0 998] /Size 998 /W [1 3 1] /Root 995 0 R /Info 996 0 R /ID [<2F478518778AF498C5AAC5643837EE4E> <2F478518778AF498C5AAC5643837EE4E>] /Length 2441 /Filter /FlateDecode >> stream x%[l]Ggs$Il'N$v87';ǎę_b'vBBB@ TEP jK"R eKT@E"*JQ9˧Yl_kl{לus3~Q,%.ͻ $h7ѺSPv uV*p ؊%h{kAn݄4Bk!vM5hh uq+G{<~J7Z6nC/,nCk ԋAۅG}hz:}`?Z; hѪ a#hz`V#`(8v886> Ю Ohh8xM 7`lv,@É48M=ΣmE/kJ;,Nۄsm΃hGAAEx,u$\ߢzZ_ !Y@,2Q5i EsKS2=&K|^/ӈ#W͍~_lFdL=H[I˦ G}[#m1` ([&^h l54 h;e}&jA#`cjB6\f[Go:AKA7]&z3u\3ibޗUta0du1utG4;QKF1OpH htܯ>tLu&΀p΃Y0Es%EP} ukXvuk`ŬXYkAM mu6SP6[h,F_lbLZкP,VB.Ե4;>Qn|W`"*2&hS(PFYdvU;)^b'iFdWᵨ_N'nj}f? |E8`͢4| }!}eţf?鐦6$Ц~#q5^6iIC*f4lFx)i،ReqΒ}ieqz%E+Req ,Z{P`8eK+L,m*eV6-y6MP6ҕ:i)J%+i[;ǨT[&3qQSM4Zhbh-d/Hꍷnג˚Xī[:%|W((0G-F48ed%yBi~mҒ&L-y]ijt3F%/]%s@m>T%xZG3L[PFCX߰ݒwztq t,t .-B X,.fC'ݖ m@"x#`gu`Ð740G~ Gy@V:%"L㖎WƁГ|Xzd`@t KbK*U8",Z8'leK7HHld G8x8zN# *kxu}5xN'.s4&!sbQ|3h4"#L^s&L<'97^@G<sk~Ӆ{pϩӆzZ cny ϖisYy7,|QT]N6~N垒9Y0.Ρ2Xl *X--2XP='zh\=><_:Z= / dX Ȱ@2,iP*2,i}m^[#V! odx#tՑQgX aLΰ:çOZS[bFV\Ԩ*V5겊>QQ{K^+eY>+kϊ'_h~Qowj4`YoqXXx=ҁcs\_:plcs^N/cs)ׇ86DZ9ql!86DZ9v9 w$}8Ź>`\hO%X4s0g?Q~/͢ endstream endobj startxref 430339 %%EOF ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5629401 JUBE-2.6.2/examples/0000775000174700017470000000000014603772011016247 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/examples/cycle/0000775000174700017470000000000014603772011017346 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/cycle/cycle.xml0000664000174700017470000000046714603772010021175 0ustar00gitlab-runnergitlab-runner A cycle example echo $jube_wp_cycle touch done ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/cycle/cycle.yaml0000664000174700017470000000043314603772010021330 0ustar00gitlab-runnergitlab-runner--- benchmark: - name: cycle outpath: bench_run comment: A cycle example step: - name: a_step cycles: 5 do: - _: echo $jube_wp_cycle break_file: done - _: touch done active: $jube_wp_cycle==2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5669403 JUBE-2.6.2/examples/dependencies/0000775000174700017470000000000014603772011020675 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/dependencies/dependencies.xml0000664000174700017470000000125114603772010024043 0ustar00gitlab-runnergitlab-runner A Dependency example 1,2,4 param_set echo $number cat first_step/stdout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/dependencies/dependencies.yaml0000664000174700017470000000071514603772010024211 0ustar00gitlab-runnergitlab-runnername: dependencies outpath: bench_run comment: A Dependency example #Configuration parameterset: name: param_set parameter: {name: number, type: int, _: "1,2,4" } #comma separated integers must be quoted #Operation step: - name: first_step use: param_set #use existing parameterset do: echo $number #shell command - name: second_step depend: first_step #Create a dependency between both steps do: cat first_step/stdout #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/do_log/0000775000174700017470000000000014603772011017512 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/do_log.xml0000664000174700017470000000072014603772010021475 0ustar00gitlab-runnergitlab-runner 1,2,3,4,5 param_set cp ../../../../loreipsum${number} shared grep -r -l "Hidden!" loreipsum* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/do_log.yaml0000664000174700017470000000056314603772010021644 0ustar00gitlab-runnergitlab-runner- name: do_log_example outpath: bench_run parameterset: - name: "param_set" parameter: - {name: "number", _: "1,2,3,4,5"} step: name: execute use: - param_set do_log_file: "do_log" shared: "shared" do: - cp ../../../../loreipsum${number} shared - {shared: "true", _: "grep -r -l \"Hidden!\" loreipsum*"} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/loreipsum10000664000174700017470000000067614603772010021545 0ustar00gitlab-runnergitlab-runnerLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/loreipsum20000664000174700017470000000067614603772010021546 0ustar00gitlab-runnergitlab-runnerLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/loreipsum30000664000174700017470000000067614603772010021547 0ustar00gitlab-runnergitlab-runnerLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/loreipsum40000664000174700017470000000070614603772010021542 0ustar00gitlab-runnergitlab-runnerLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Hidden! Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/do_log/loreipsum50000664000174700017470000000067614603772010021551 0ustar00gitlab-runnergitlab-runnerLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/duplicate/0000775000174700017470000000000014603772011020221 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/duplicate/duplicate.xml0000664000174700017470000000130514603772010022713 0ustar00gitlab-runnergitlab-runner parameter duplicate example 1 2,3,4 20,30,40 int(${iterations}*(${iterations}+1)/2) options,result echo $sum ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/duplicate/duplicate.yaml0000664000174700017470000000076114603772010023062 0ustar00gitlab-runnergitlab-runnername: parameter_duplicate_example outpath: bench_run comment: parameter duplicate example parameterset: - name: options duplicate: concat parameter: - {name: iterations, _: "1"} - {name: iterations, tag: few, _: "2,3,4"} - {name: iterations, tag: many, _: "20,30,40"} - name: result parameter: - {name: sum, mode: "python", _: "int(${iterations}*(${iterations}+1)/2)"} step: name: perform_iterations use: "options,result" do: "echo $sum" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/environment/0000775000174700017470000000000014603772011020613 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/environment/environment.xml0000664000174700017470000000147414603772010023706 0ustar00gitlab-runnergitlab-runner An environment handling example VALUE export SHELL_VAR=Hello echo "$$SHELL_VAR world" param_set echo $$EXPORT_ME echo "$$SHELL_VAR again" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/environment/environment.yaml0000664000174700017470000000110214603772010024034 0ustar00gitlab-runnergitlab-runnername: environment outpath: bench_run comment: An environment handling example #Configuration parameterset: name: param_set parameter: {name: EXPORT_ME, export: true, _: VALUE} step: #Operation - name: first_step export: true do: - export SHELL_VAR=Hello #export a Shell var - echo "$$SHELL_VAR world" #use exported Shell var #Create a dependency between both steps - name: second_step depend: first_step use: param_set do: - echo $$EXPORT_ME - echo "$$SHELL_VAR again" #use exported Shell var out of privious step ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/files_and_sub/0000775000174700017470000000000014603772011021044 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/files_and_sub/file.in0000664000174700017470000000003614603772010022311 0ustar00gitlab-runnergitlab-runnerNumber: #NUMBER# Zahl: #ZAHL# ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/files_and_sub/files_and_sub.xml0000664000174700017470000000207314603772010024364 0ustar00gitlab-runnergitlab-runner A file copy and substitution example 1,2,4 2,4,5 file.in param_set files substitute cat file.out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/files_and_sub/files_and_sub.yaml0000664000174700017470000000143414603772010024526 0ustar00gitlab-runnergitlab-runnername: files_and_sub outpath: bench_run comment: A file copy and substitution example #Configuration parameterset: name: param_set parameter: - {name: number, type: int, _: "1,2,4"} #comma separated integers must be quoted - {name: zahl, type: int, _: "2,4,5"} #comma separated integers must be quoted #Files fileset: name: files copy: file.in #Substitute substituteset: name: substitute iofile: {in: file.in, out: file.out} sub: - {source: "#NUMBER#", dest: $number} #"#" must be quoted - {source: "#[^NUMBER]+#", dest: $zahl, mode: "regex"} #"#" must be quoted #Operation step: name: sub_step use: - param_set #use existing parameterset - files #use existing fileset - substitute #use existing substituteset do: cat file.out #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/hello_world/0000775000174700017470000000000014603772011020561 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/hello_world/hello_world.xml0000664000174700017470000000077414603772010023624 0ustar00gitlab-runnergitlab-runner A simple hello world Hello World hello_parameter echo $hello_str ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/hello_world/hello_world.yaml0000664000174700017470000000044114603772010023755 0ustar00gitlab-runnergitlab-runnername: hello_world outpath: bench_run comment: A simple hello world #Configuration parameterset: name: hello_parameter parameter: {name: hello_str, _: Hello World} #Operation step: name: say_hello use: hello_parameter #use existing parameter do: echo $hello_str #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/include/0000775000174700017470000000000014603772011017672 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/include/include_data.xml0000664000174700017470000000052214603772010023026 0ustar00gitlab-runnergitlab-runner 1,2,4 Hello echo Test echo $number ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/include/include_data.yaml0000664000174700017470000000027014603772010023170 0ustar00gitlab-runnergitlab-runnerparameterset: - name: param_set parameter: {name: number, type: int, _: "1,2,4"} - name: param_set2 parameter: {name: text, _: Hello} dos: - echo Test - echo $number ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/include/main.xml0000664000174700017470000000133014603772010021334 0ustar00gitlab-runnergitlab-runner A include example bar param_set param_set2 echo $foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/include/main.yaml0000664000174700017470000000074114603772010021503 0ustar00gitlab-runnergitlab-runnername: include outpath: bench_run comment: A include example #use parameterset out of an external file and add a additional parameter parameterset: name: param_set init_with: include_data.yaml parameter: {name: foo, _: bar} #Operation step: name: say_hello use: - param_set #use existing parameterset - from: include_data.yaml _: param_set2 #out of an external file do: - echo $foo - !include include_data.yaml:["dos"] #include all available tag ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/iterations/0000775000174700017470000000000014603772011020430 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/iterations/iterations.xml0000664000174700017470000000253414603772010023336 0ustar00gitlab-runnergitlab-runner A Iteration example 1,2,4 $foo iter:$jube_wp_iteration param_set echo $bar echo $bar analyse analyse_no_reduce

jube_res_analyser jube_wp_id_first_step jube_wp_id jube_wp_iteration_first_step jube_wp_iteration foo
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/iterations/iterations.yaml0000664000174700017470000000163614603772010023502 0ustar00gitlab-runnergitlab-runnername: iterations outpath: bench_run comment: A Iteration example #Configuration parameterset: name: param_set parameter: - {name: foo, type: int, _: "1,2,4"} - {name: bar, update_mode: step, _: '$foo iter:$jube_wp_iteration'} step: - name: first_step iterations: 2 use: param_set #use existing parameterset do: echo $bar #shell command - name: second_step depend: first_step iterations: 2 do: echo $bar #shell command analyser: #analyse without reduce - name: analyse_no_reduce reduce: false analyse: step: second_step #analyse with reduce - name: analyse reduce: true analyse: step: second_step result: use: [analyse,analyse_no_reduce] table: name: result style: pretty column: - jube_res_analyser - jube_wp_id_first_step - jube_wp_id - jube_wp_iteration_first_step - jube_wp_iteration - foo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/jobsystem/0000775000174700017470000000000014603772011020266 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/jobsystem/job.run.in0000664000174700017470000000034514603772010022174 0ustar00gitlab-runnergitlab-runner#!/bin/bash -x #MSUB -l nodes=#NODES#:ppn=#PROCS_PER_NODE# #MSUB -l walltime=#WALLTIME# #MSUB -e #ERROR_FILEPATH# #MSUB -o #OUT_FILEPATH# #MSUB -M #MAIL_ADDRESS# #MSUB -m #MAIL_MODE# ### start of jobscript #EXEC# touch #READY# ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/jobsystem/jobsystem.xml0000664000174700017470000000376214603772010023036 0ustar00gitlab-runnergitlab-runner A jobsystem example 1,2,4 msub job.run 1 00:01:00 4 ready abe stderr stdout echo $number ${job_file}.in param_set executeset files,sub_job $submit_cmd $job_file ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/jobsystem/jobsystem.yaml0000664000174700017470000000313314603772010023170 0ustar00gitlab-runnergitlab-runnername: jobsystem outpath: bench_run comment: A jobsystem example parameterset: #benchmark configuration - name: param_set parameter: {name: number, type: int, _: "1,2,4"} #comma separated integer must be quoted #Job configuration - name: executeset parameter: - {name: submit_cmd, "_": msub} - {name: job_file, "_": job.run} - {name: nodes, type: int, "_": 1} - {name: walltime, "_": "00:01:00"} #: must be quoted - {name: ppn, type: int, "_": 4} - {name: ready_file, "_": ready} - {name: mail_mode, "_": abe} - {name: mail_address} - {name: err_file, "_": stderr} - {name: out_file, "_": stdout} - {name: exec, "_": echo $number} #Load jobfile fileset: name: files copy: ${job_file}.in substituteset: name: sub_job iofile: {in: "${job_file}.in", out: $job_file} #attributes with {} must be quoted sub: - {source: "#NODES#", dest: $nodes} - {source: "#PROCS_PER_NODE#", dest: $ppn} - {source: "#WALLTIME#", dest: $walltime} - {source: "#ERROR_FILEPATH#", dest: $err_file} - {source: "#OUT_FILEPATH#", dest: $out_file} - {source: "#MAIL_ADDRESS#", dest: $mail_address} - {source: "#MAIL_MODE#", dest: $mail_mode} - {source: "#EXEC#", dest: $exec} - {source: "#READY#", _: $ready_file } # _ can be used here as well instead of dest (should be used for multiline output) #Operation step: name: submit work_dir: "$$WORK/jobsystem_bench_${jube_benchmark_id}_${jube_wp_id}" use: [param_set,executeset,files,sub_job] do: done_file: $ready_file _: $submit_cmd $job_file #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/parallel_workpackages/0000775000174700017470000000000014603772011022604 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parallel_workpackages/parallel_workpackages.xml0000664000174700017470000000111114603772010027654 0ustar00gitlab-runnergitlab-runner A parallel workpackages demo ",".join([ str(i) for i in range(0,10)]) param_set echo "${i}" N=1000000 ; a=1; k=0; while [ "$k" -lt $N ]; do echo $(( 2*k + 1 + $a )) ; k=$(( k + 1 )) ; done ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parallel_workpackages/parallel_workpackages.yaml0000664000174700017470000000064614603772010030032 0ustar00gitlab-runnergitlab-runnername: parallel_workpackages outpath: bench_run comment: A parallel workpackages demo parameterset: name: param_set parameter: {name: i, type: int, mode: python, _: "\",\".join([ str(i) for i in range(0,10)])"} step: name: parallel_execution suffix: ${i} procs: 4 use: param_set do: - "echo \"${i}\"" - "N=1000000 ; a=1; k=0; while [ \"$k\" -lt $N ]; do echo $(( 2*k + 1 + $a )) ; k=$(( k + 1 )) ; done" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/parameter_dependencies/0000775000174700017470000000000014603772011022735 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_dependencies/include_file.xml0000664000174700017470000000043614603772010026103 0ustar00gitlab-runnergitlab-runner 10 20 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_dependencies/include_file.yaml0000664000174700017470000000025114603772010026240 0ustar00gitlab-runnergitlab-runnerparameterset: - name: depend_param_set0 parameter: {name: number2, type: int, _: 10} - name: depend_param_set1 parameter: {name: number2, type: int, _: 20} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_dependencies/parameter_dependencies.xml0000664000174700017470000000177314603772010030154 0ustar00gitlab-runnergitlab-runner A parameter_dependencies example 0,1 ["hello","world"][$index] 3,5 1,2,4 param_set depend_param_set$index depend_param_set$index echo "$text $number $number2" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_dependencies/parameter_dependencies.yaml0000664000174700017470000000157214603772010030313 0ustar00gitlab-runnergitlab-runnername: parameter_dependencies outpath: bench_run comment: A parameter_dependencies example #Configuration parameterset: - name: param_set parameter: - {name: index, type: int, _: "0,1"} #comma separated integer must be in quotations - {name: text, mode: python, _: '["hello","world"][$index]'} #attributes with " and [] must be in quotations - name: depend_param_set0 parameter: {name: number, type: int, _: "3,5"} #comma separated integer must be in quotations - name: depend_param_set1 parameter: {name: number, type: int, _: "1,2,4"} #comma separated integer must be in quotations #Operation step: name: operation use: - param_set #use basic parameterset - depend_param_set$index #use dependent parameterset - {from: 'include_file.yaml:depend_param_set0:depend_param_set1', _: depend_param_set$index} do: echo "$text $number $number2" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/parameter_update/0000775000174700017470000000000014603772011021571 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_update/parameter_update.xml0000775000174700017470000000206514603772010025642 0ustar00gitlab-runnergitlab-runner A parameter_update example iter_never: $jube_wp_id iter_use: $jube_wp_id iter_step: $jube_wp_id foo echo $bar_never echo $bar_use echo $bar_step foo echo $bar_never echo $bar_use echo $bar_step echo $bar_never echo $bar_use echo $bar_step ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameter_update/parameter_update.yaml0000664000174700017470000000135014603772010025775 0ustar00gitlab-runnergitlab-runnername: parameter_updates outpath: bench_run comment: A parameter_update example #Configuration parameterset: name: foo parameter: - {name: bar_never, mode: text, update_mode: never, _: "iter_never: $jube_wp_id"} - {name: bar_use, mode: text, update_mode: use, _: "iter_use: $jube_wp_id"} - {name: bar_step, mode: text, update_mode: step, _: "iter_step: $jube_wp_id"} #Operation step: - name: step1 use: foo do: - echo $bar_never - echo $bar_use - echo $bar_step - name: step2 depend: step1 use: foo do: - echo $bar_never - echo $bar_use - echo $bar_step - name: step3 depend: step2 do: - echo $bar_never - echo $bar_use - echo $bar_step ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/parameterspace/0000775000174700017470000000000014603772011021243 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameterspace/parameterspace.xml0000664000174700017470000000121214603772010024754 0ustar00gitlab-runnergitlab-runner A parameterspace example 1,2,4 Hello;World param_set echo "$text $number" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/parameterspace/parameterspace.yaml0000664000174700017470000000067714603772010025134 0ustar00gitlab-runnergitlab-runnername: parameterspace outpath: bench_run comment: A parameterspace example #Configuration parameterset: name: param_set #Create a parameterspace out of two template parameter parameter: - {name: number, type: int, _: "1,2,4"} #comma separated integer must be quoted - {name: text, separator: ;, _: Hello;World} #Operation step: name: say_hello use: param_set #use existing parameterset do: echo "$text $number" #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/result_creation/0000775000174700017470000000000014603772011021451 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/result_creation/result_creation.xml0000664000174700017470000000361514603772010025401 0ustar00gitlab-runnergitlab-runner A result creation example 1,2,4 .*? $jube_pat_int Number: $jube_pat_int Zahl: $jube_pat_int param_set echo "Number: $number" > en echo "Zahl: $number" > de pattern_all en de analyse numbernumber_patnumber_pat_ennumber_pat_de
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/result_creation/result_creation.yaml0000664000174700017470000000251314603772010025537 0ustar00gitlab-runnergitlab-runnername: result_creation outpath: bench_run comment: A result creation example #Configuration parameterset: name: param_set #Create a parameterspace with one template parameter parameter: {name: number, type: int, _: "1,2,4"} # comma separated integer must be quoted #Regex pattern patternset: - name: pattern_all pattern: - {name: number_pat, type: int, _: ".*? $jube_pat_int"} # "?" must be quoted - name: pattern_en pattern: - {name: number_pat_en, type: int, _: "Number: $jube_pat_int"} # ":" must be quoted - name: pattern_de pattern: - {name: number_pat_de, type: int, _: "Zahl: $jube_pat_int"} #Operation step: name: write_number use: param_set #use existing parameterset do: - 'echo "Number: $number" > en' - 'echo "Zahl: $number" > de' #shell commands #Analyse analyser: - name: analyse use: pattern_all #use existing patternset for all files analyse: step: write_number file: - use: pattern_en #use patternset only for this file _: en - use: pattern_de _: de #Create result table result: use: analyse #use existing analyser table: name: result style: pretty sort: number column: - number - number_pat # Column with title same as pattern name - { title: "Number", _: number_pat_en} # Column with costum title - { title: "Zahl", _: number_pat_de} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5709403 JUBE-2.6.2/examples/result_database/0000775000174700017470000000000014603772011021411 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/result_database/result_database.xml0000664000174700017470000000221714603772010025276 0ustar00gitlab-runnergitlab-runner result database creation 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse number number_pat ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/result_database/result_database.yaml0000664000174700017470000000145314603772010025441 0ustar00gitlab-runnergitlab-runnername: result_database outpath: bench_run comment: result database creation parameterset: name: param_set parameter: {name: number, type: int, _: "1,2,4"} patternset: name: pattern pattern: {name: number_pat, type: int, _: "Number: $jube_pat_int"} step: name: write_number use: param_set do: "echo \"Number: $number\"" analyser: name: analyse use: pattern analyse: step: write_number file: stdout result: use: analyse database: # creating a database containing the columns "number" and "number_pat" # one table of the name "results" is created within the database # optionally, you can use the "file" attribute to specify an alternative storage location for the database name: results primekeys: "number,number_pat" key: - number - number_pat ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/result_database/result_database_filter.xml0000664000174700017470000000242414603772010026643 0ustar00gitlab-runnergitlab-runner result database creation 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse number number_pat ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/scripting_parameter/0000775000174700017470000000000014603772011022311 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/scripting_parameter/scripting_parameter.xml0000664000174700017470000000213214603772010027072 0ustar00gitlab-runnergitlab-runner A scripting parameter example 1,2,4 ",".join(str(a*${number}) for a in [1,2]) ${number}*${additional_number} Number: $number param_set echo "number: $number, additional_number: $additional_number" echo "number_mult: $number_mult, text: $text" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/scripting_parameter/scripting_parameter.yaml0000664000174700017470000000137314603772010027242 0ustar00gitlab-runnergitlab-runnername: scripting_parameter outpath: bench_run comment: A scripting parameter example #Configuration parameterset: name: param_set parameter: #Normal template - {name: number, type: int, _: "1,2,4"} #A template created by a scripting parameter - {name: additional_number, mode: python, type: int, _: '",".join(str(a*${number}) for a in [1,2])'} #A scripting parameter - {name: number_mult, mode: python, type: float, _: "${number}*${additional_number}"} #Reuse another parameter - {name: text, _: "Number: $number"} #Operation step: name: operation use: param_set #use existing parameterset do: - 'echo "number: $number, additional_number: $additional_number"' - 'echo "number_mult: $number_mult, text: $text"' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/scripting_pattern/0000775000174700017470000000000014603772011022006 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/scripting_pattern/scripting_pattern.xml0000664000174700017470000000371414603772010026273 0ustar00gitlab-runnergitlab-runner A scripting_pattern example 0,1,2 param_set echo "$value" $jube_pat_int $value_pat+$value pattern_not_available: $jube_pat_int $missing_pat*$value pattern_not_available: $jube_pat_int $missing_pat_def*$value pattern_set stdout analyse valuevalue_patdep_patmissing_patmissing_dep_patmissing_pat_defmissing_def_dep_pat
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/scripting_pattern/scripting_pattern.yaml0000664000174700017470000000242614603772010026434 0ustar00gitlab-runnergitlab-runnername: scripting_pattern outpath: bench_run comment: A scripting_pattern example #Configuration parameterset: name: param_set parameter: {name: value, type: int, _: "0,1,2"} #Operation step: name: operation use: param_set do: echo "$value" #Pattern to extract patternset: name: pattern_set pattern: #A normal pattern - {name: value_pat, type: int, _: $jube_pat_int} #A combination of a pattern and a parameter - {name: dep_pat, type: int, mode: python, _: $value_pat+$value} #This pattern is not available - {name: missing_pat, type: int, _: "pattern_not_available: $jube_pat_int"} #The combination will fail (create NaN) - {name: missing_dep_pat, type: int, mode: python, _: $missing_pat*$value} #Default value for missing pattern - {name: missing_pat_def, type: int, default: 0, _: "pattern_not_available: $jube_pat_int"} #Combination of default value and parameter - {name: missing_def_dep_pat, type: int, mode: python, _: $missing_pat_def*$value} analyser: name: analyse use: pattern_set analyse: step: operation file: stdout #result table creation result: use: analyse table: name: result style: pretty column: [value,value_pat,dep_pat,missing_pat,missing_dep_pat,missing_pat_def,missing_def_dep_pat] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/shared/0000775000174700017470000000000014603772011017515 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/shared/shared.xml0000664000174700017470000000114414603772010021504 0ustar00gitlab-runnergitlab-runner A shared folder example 1,2,4 param_set echo $jube_wp_id >> shared/all_ids cat all_ids ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/shared/shared.yaml0000664000174700017470000000060314603772010021645 0ustar00gitlab-runnergitlab-runnername: shared outpath: bench_run comment: A shared folder example #Configuration parameterset: name: param_set parameter: {name: number, type: int, _: "1,2,4"} #Operation step: name: a_step shared: shared use: param_set do: - echo $jube_wp_id >> shared/all_ids #shell command will run three times - {shared: true, _: cat all_ids} #shell command will run one times ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/statistic/0000775000174700017470000000000014603772011020256 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/statistic/statistic.xml0000664000174700017470000000273014603772010023010 0ustar00gitlab-runnergitlab-runner A result reduce example $jube_pat_int echo "1 2 3 4 5 6 7 8 9 10" pattern stdout analyse number_patnumber_pat_firstnumber_pat_lastnumber_pat_minnumber_pat_maxnumber_pat_sumnumber_pat_cntnumber_pat_avgnumber_pat_std
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/statistic/statistic.yaml0000664000174700017470000000165314603772010023155 0ustar00gitlab-runnergitlab-runnername: reduce_example outpath: bench_run comment: A result reduce example #Regex pattern patternset: name: pattern pattern: {name: number_pat, type: int, _: $jube_pat_int} #Operation step: name: write_some_numbers do: echo "1 2 3 4 5 6 7 8 9 10" #shell command #Analyse analyser: name: analyse use: pattern #use existing patternset analyse: step: write_some_numbers file: stdout #file which should be scanned #Create result table result: use: analyse #use existing analyser table: name: result style: pretty column: - number_pat #first match - number_pat_first #first match - number_pat_last #last match - number_pat_min #min of all matches - number_pat_max #max of all matches - number_pat_sum #sum of all matches - number_pat_cnt #number of matches - number_pat_avg #avg of all matches - {_: number_pat_std, format: .2f} #std of all matches ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/tagging/0000775000174700017470000000000014603772011017667 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/tagging/tagging.xml0000664000174700017470000000132714603772010022033 0ustar00gitlab-runnergitlab-runner deu|eng Tags as logical combination Hello Hallo World param_set echo '$hello_str $world_str' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/tagging/tagging.yaml0000664000174700017470000000071514603772010022175 0ustar00gitlab-runnergitlab-runnercheck_tags: deu|eng #check if tag deu or eng was set name: tagging outpath: bench_run comment: Tags as logical combination #Configuration parameterset: name: param_set parameter: - {name: hello_str, tag: "!deu+eng", _: Hello} - {name: hello_str, tag: deu|!eng, _: Hallo} - {name: world_str, tag: eng, _: World} #Operation step: name: say_hello use: param_set #use existing parameterset do: echo '$hello_str $world_str' #shell command ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/examples/yaml/0000775000174700017470000000000014603772011017211 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/yaml/hello_world.yaml0000664000174700017470000000072614603772010022413 0ustar00gitlab-runnergitlab-runnerbenchmark: # having only a single benchmark, this key is optional name: hello_world outpath: bench_run comment: A simple hello world in yaml #Configuration parameterset: name: hello_parameter parameter: {name: hello_str, _: Hello World} #Operation step: name: say_hello use: hello_parameter # special key _ can be skipped do: - _: echo $hello_str # - is optional in this case, as ther is only one do entry active: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/examples/yaml/special_values.yaml0000664000174700017470000000116514603772010023076 0ustar00gitlab-runnergitlab-runnername: special values outpath: bench_run comment: An example for values that need to be in quotations parameterset: name: special_parameters parameter: - {name: integer, type: int, _: "1,2,4"} #comma seperated values need to be quoted - {name: "NUMBER", _: "#3"} #values with # need to be quoted patternset: name: special_pattern pattern: - {name: result, type: int, _: "Result: test"} #values with : need to be quoted - {name: integers, type: int, _: "Integers = {$integer}"} #values with {} need to be quoted - {name: integer, type: int, _: "'Integer' = $NUMBER"} #values with ' need to be quoted ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5749402 JUBE-2.6.2/jube2/0000775000174700017470000000000014603772011015440 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/__init__.py0000664000174700017470000000143514603772010017553 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2 package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/analyser.py0000664000174700017470000005500514603772010017634 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Analyser class handles the analyse process""" from __future__ import (print_function, unicode_literals, division) import xml.etree.ElementTree as ET import jube2.log import os import re import glob import math import jube2.pattern import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class Analyser(object): """The Analyser handles the analyse process and store all important data to run a new analyse.""" class AnalyseFile(object): """A file which should be analysed""" def __init__(self, path): self._path = path self._use = set() def add_uses(self, use_names): """Add an addtional patternset name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) def __eq__(self, other): result = len(self._use.symmetric_difference(other.use)) == 0 return result and (self._path == other.path) def __repr__(self): return "AnalyseFile({0})".format(self._path) @property def use(self): """Return uses""" return self._use @property def path(self): """Get file path""" return self._path def etree_repr(self): """Return etree object representation""" file_etree = ET.Element("file") file_etree.text = self._path if len(self._use) > 0: file_etree.attrib["use"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._use) return file_etree def __init__(self, name, reduce_iteration=True): self._name = name self._use = set() self._analyse = dict() self._benchmark = None self._analyse_result = None self._reduce_iteration = reduce_iteration @property def benchmark(self): """Get benchmark information""" return self._benchmark @benchmark.setter def benchmark(self, benchmark): """Set benchmark information""" self._benchmark = benchmark @property def use(self): """Return uses""" return self._use @property def analyser(self): """Return analyse dict""" return self._analyse @property def analyse_result(self): """Return analyse result""" return self._analyse_result @analyse_result.setter def analyse_result(self, analyse_result): """Set analyse result""" self._analyse_result = analyse_result def add_analyse(self, step_name, analyse_file): """Add an addtional analyse file""" if step_name not in self._analyse: self._analyse[step_name] = list() if (analyse_file not in self._analyse[step_name]) and \ (analyse_file is not None): self._analyse[step_name].append(analyse_file) def add_uses(self, use_names): """Add an addtional patternset name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) @property def name(self): """Get analyser name""" return self._name def etree_repr(self): """Return etree object representation""" analyser_etree = ET.Element("analyser") analyser_etree.attrib["name"] = self._name analyser_etree.attrib["reduce"] = str(self._reduce_iteration) for use in self._use: use_etree = ET.SubElement(analyser_etree, "use") use_etree.text = use for step_name in self._analyse: analyse_etree = ET.SubElement(analyser_etree, "analyse") analyse_etree.attrib["step"] = step_name for fileobj in self._analyse[step_name]: analyse_etree.append(fileobj.etree_repr()) return analyser_etree def _combine_and_check_patternsets(self, patternset, uses): """Combine patternsets given by uses and check compatibility""" for use in uses: if use not in self._benchmark.patternsets: raise RuntimeError((" used but not " + "found").format(use)) if not patternset.is_compatible(self._benchmark.patternsets[use]): incompatible_names = patternset.get_incompatible_pattern( self._benchmark.patternsets[use]) raise RuntimeError(("Cannot use patternset \"{0}\" " + "in analyser \"{1}\", because there are " + "incompatible pattern name combinations: " "{2}") .format(use, self._name, ",".join(incompatible_names))) patternset.add_patternset(self._benchmark.patternsets[use]) def analyse(self): """Run the analyser""" LOGGER.debug("Run analyser \"{0}\"".format(self._name)) if self._benchmark is None: raise RuntimeError("No benchmark found using analyser {0}" .format(self._name)) result = dict() # Combine all patternsets patternset = jube2.pattern.Patternset() self._combine_and_check_patternsets(patternset, self._use) # Print debug info debugstr = " available pattern:\n" debugstr += \ jube2.util.output.text_table( [("pattern", "value")] + sorted([(par.name, par.value) for par in patternset.pattern_storage]), use_header_line=True, indent=9, align_right=False) debugstr += "\n available derived pattern:\n" debugstr += \ jube2.util.output.text_table( [("pattern", "value")] + sorted([(par.name, par.value) for par in patternset.derived_pattern_storage]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) for stepname in self._analyse: result[stepname] = dict() LOGGER.debug(" analyse step \"{0}\"".format(stepname)) if stepname not in self._benchmark.steps: raise RuntimeError(("Could not find " "when using analyser \"{1}\"").format( stepname, self._name)) step = self._benchmark.steps[stepname] workpackages = set(self._benchmark.workpackages[stepname]) while len(workpackages) > 0: root_workpackage = workpackages.pop() match_dict = dict() # Global patternset to store all existing pattern (e.g. from # individual file uses), necessary to evaluate default pattern # and derived pattern global_patternset = patternset.copy() result[stepname][root_workpackage.id] = dict() # Should multiple iterations be reduced to a single result line if self._reduce_iteration: siblings = set(root_workpackage.iteration_siblings) else: siblings = set([root_workpackage]) while len(siblings) > 0: workpackage = siblings.pop() if workpackage in workpackages: workpackages.remove(workpackage) # Ignore workpackages not started yet if not workpackage.started: continue parameter = \ dict([[par.name, par.value] for par in workpackage.parameterset. constant_parameter_dict.values()]) for file_obj in self._analyse[stepname]: if step.alt_work_dir is not None: file_path = step.alt_work_dir file_path = jube2.util.util.substitution( file_path, parameter) file_path = \ os.path.expandvars( os.path.expanduser(file_path)) file_path = os.path.join( self._benchmark.file_path_ref, file_path) else: file_path = workpackage.work_dir filename = \ jube2.util.util.substitution(file_obj.path, parameter) filename = \ os.path.expandvars(os.path.expanduser(filename)) file_path = os.path.join(file_path, filename) for path in glob.glob(file_path): # scan files LOGGER.debug((" scan file {0}").format(path)) new_result_dict, match_dict = \ self._analyse_file(path, patternset, global_patternset, workpackage.parameterset, match_dict, file_obj.use) result[stepname][root_workpackage.id].update( new_result_dict) # Set default pattern values if available and necessary new_result_dict = result[stepname][root_workpackage.id] for pattern in global_patternset.pattern_storage: if (pattern.default_value is not None) and \ (pattern.name not in new_result_dict): default = pattern.default_value # Convert default value if pattern.content_type == "int": if default == "nan": default = float("nan") else: default = int(float(default)) elif pattern.content_type == "float": default = float(default) new_result_dict[pattern.name] = default new_result_dict[pattern.name + "_cnt"] = 0 new_result_dict[pattern.name + "_first"] = default new_result_dict[pattern.name + "_last"] = default if pattern.content_type in ["int", "float"]: new_result_dict.update( {pattern.name + "_sum": default, pattern.name + "_min": default, pattern.name + "_max": default, pattern.name + "_avg": default, pattern.name + "_sum2": default ** 2, pattern.name + "_std": 0}) # Evaluate derived pattern new_result_dict = self._eval_derived_pattern( global_patternset, root_workpackage.parameterset, result[stepname][root_workpackage.id]) result[stepname][root_workpackage.id].update( new_result_dict) self._analyse_result = result def _eval_derived_pattern(self, patternset, parameterset, result_dict): """Evaluate all derived pattern in patternset using parameterset and result_dict""" resultset = jube2.parameter.Parameterset() for name in result_dict: resultset.add_parameter( jube2.parameter.Parameter.create_parameter( name, value=str(result_dict[name]))) # Get jube patternset jube_pattern = jube2.pattern.get_jube_pattern() # calculate derived pattern patternset.derived_pattern_substitution( [parameterset, resultset, jube_pattern.pattern_storage]) new_result_dict = dict() # Convert content type for par in patternset.derived_pattern_storage: if par.mode not in jube2.conf.ALLOWED_SCRIPTTYPES: new_result_dict[par.name] = \ jube2.util.util.convert_type(par.content_type, par.value, stop=False) return new_result_dict def _analyse_file(self, file_path, patternset, global_patternset, parameterset, match_dict=None, additional_uses=None): """Scan given files with given pattern and produce a result parameterset""" if additional_uses is None: additional_uses = set() if match_dict is None: match_dict = dict() if not os.path.isfile(file_path): return dict(), match_dict local_patternset = patternset.copy() # Add file specific uses self._combine_and_check_patternsets(local_patternset, additional_uses) self._combine_and_check_patternsets(global_patternset, additional_uses) # Unique pattern/parameter check if (not parameterset.is_compatible( local_patternset.pattern_storage)) or \ (not parameterset.is_compatible( local_patternset.derived_pattern_storage)): incompatible_names = parameterset.get_incompatible_parameter( local_patternset.pattern_storage) incompatible_names.update(parameterset.get_incompatible_parameter( local_patternset.derived_pattern_storage)) raise RuntimeError(("A pattern and a parameter (\"{0}\") " "using the same name in " "analyser \"{1}\"").format( ",".join(incompatible_names), self._name)) # Get jube patternset jube_pattern = jube2.pattern.get_jube_pattern() # Do pattern substitution local_patternset.pattern_substitution( [parameterset, jube_pattern.pattern_storage]) patternlist = [p for p in local_patternset.pattern_storage] file_handle = open(file_path, "r") # Read file content data = file_handle.read() for pattern in patternlist: if pattern.name not in match_dict: match_dict[pattern.name] = dict() try: mode = re.MULTILINE if pattern.dotall: mode += re.DOTALL regex = re.compile(pattern.value, mode) except re.error as ree: raise RuntimeError(("Error inside pattern \"{0}\" : " + "\"{1}\" : {2}") .format(pattern.name, pattern.value, ree)) # Run regular expression matches = re.findall(regex, data) # If there are different groups reduce result shape if regex.groups > 1: match_list = list() for match in matches: match_list = match_list + list(match) else: match_list = matches # Remove empty matches match_list = [match for match in match_list if match != ""] # Convert to pattern type new_match_list = list() for match in match_list: try: if pattern.content_type == "int": if match == "nan": new_match_list.append(float("nan")) else: new_match_list.append(int(float(match))) elif pattern.content_type == "float": new_match_list.append(float(match)) else: new_match_list.append(match) except ValueError: LOGGER.warning(("\"{0}\" cannot be represented " + "as a \"{1}\"") .format(match, pattern.content_type)) match_list = new_match_list if len(match_list) > 0: # First match is default if "first" not in match_dict[pattern.name]: match_dict[pattern.name]["first"] = match_list[0] for match in match_list: if pattern.content_type in ["int", "float"]: if "min" in match_dict[pattern.name]: match_dict[pattern.name]["min"] = \ min(match_dict[pattern.name]["min"], match) else: match_dict[pattern.name]["min"] = match if "max" in match_dict[pattern.name]: match_dict[pattern.name]["max"] = \ max(match_dict[pattern.name]["max"], match) else: match_dict[pattern.name]["max"] = match if "sum" in match_dict[pattern.name]: match_dict[pattern.name]["sum"] += match else: match_dict[pattern.name]["sum"] = match try: if "sum2" in match_dict[pattern.name]: match_dict[pattern.name]["sum2"] += match ** 2 else: match_dict[pattern.name]["sum2"] = match ** 2 except OverflowError: LOGGER.warning( "Squared sum cannot be represented, " + "numerical result out of range.") match_dict[pattern.name]["sum2"] = math.nan if "cnt" in match_dict[pattern.name]: match_dict[pattern.name]["cnt"] += 1 else: match_dict[pattern.name]["cnt"] = 1 if pattern.content_type in ["int", "float"]: if match_dict[pattern.name]["cnt"] > 0: match_dict[pattern.name]["avg"] = \ (match_dict[pattern.name]["sum"] / match_dict[pattern.name]["cnt"]) if match_dict[pattern.name]["cnt"] > 1: try: match_dict[pattern.name]["std"] = math.sqrt( (abs(match_dict[pattern.name]["sum2"] - (match_dict[pattern.name]["sum"] ** 2 / match_dict[pattern.name]["cnt"])) / (match_dict[pattern.name]["cnt"] - 1))) except OverflowError: match_dict[pattern.name]["std"] = 0 else: match_dict[pattern.name]["std"] = 0 match_dict[pattern.name]["last"] = match_list[-1] info_str = " file \"{0}\" scanned pattern found:\n".format( os.path.basename(file_path)) info_str += jube2.util.output.text_table( [(_name, ", ".join(["{0}:{1}".format(key, con) for key, con in value.items()])) for _name, value in match_dict.items()], indent=9, align_right=True, auto_linebreak=True) LOGGER.debug(info_str) file_handle.close() # Create result dict result_dict = dict() for pattern_name in match_dict: for option in match_dict[pattern_name]: if option == "first": result_dict[pattern_name] = \ match_dict[pattern_name][option] name = "{0}_{1}".format(pattern_name, option) result_dict[name] = match_dict[pattern_name][option] return result_dict, match_dict def analyse_etree_repr(self): """Create an etree representation of a analyse dict: stepname -> workpackage_id -> filename -> patternname -> value """ etree = list() if self._analyse_result is None: return etree for stepname in self._analyse_result: step_etree = ET.Element("step") step_etree.attrib["name"] = stepname for workpackage_id in self._analyse_result[stepname]: workpackage_etree = ET.SubElement(step_etree, "workpackage") workpackage_etree.attrib["id"] = str(workpackage_id) for pattern in self._analyse_result[stepname][workpackage_id]: if type(self._analyse_result[stepname][workpackage_id] [pattern]) is int: content_type = "int" elif type(self._analyse_result[stepname][ workpackage_id][pattern]) is float: content_type = "float" else: content_type = "string" pattern_etree = ET.SubElement(workpackage_etree, "pattern") pattern_etree.attrib["name"] = pattern pattern_etree.attrib["type"] = content_type pattern_etree.text = \ str(self._analyse_result[stepname][workpackage_id] [pattern]) etree.append(step_etree) return etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/benchmark.py0000664000174700017470000010375114603772010017752 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Benchmark class manages the benchmark process""" from __future__ import (print_function, unicode_literals, division) import multiprocessing as mp import xml.etree.ElementTree as ET import xml.dom.minidom as DOM import logging import os import re import stat import pprint import shutil import itertools import jube2.parameter import jube2.util.util import jube2.util.output import jube2.conf import jube2.log LOGGER = jube2.log.get_logger(__name__) class Benchmark(object): """The Benchmark class contains all data to run a benchmark""" def __init__(self, name, outpath, parametersets, substitutesets, filesets, patternsets, steps, analyser, results, results_order, comment="", tags=None, file_path_ref="."): self._name = name self._outpath = outpath self._parametersets = parametersets self._substitutesets = substitutesets self._filesets = filesets self._patternsets = patternsets self._steps = steps self._analyser = analyser for analyser in self._analyser.values(): analyser.benchmark = self self._results = results self._results_order = results_order for result in self._results.values(): result.benchmark = self self._workpackages = dict() self._work_stat = jube2.util.util.WorkStat() self._comment = comment self._id = -1 self._file_path_ref = file_path_ref if tags is None: self._tags = set() else: self._tags = tags @property def name(self): """Return benchmark name""" return self._name @property def comment(self): """Return comment string""" return self._comment @property def tags(self): """Return set of tags""" return self._tags @comment.setter def comment(self, new_comment): """Set new comment string""" self._comment = new_comment @property def parametersets(self): """Return parametersets""" return self._parametersets @property def patternsets(self): """Return patternsets""" return self._patternsets @property def analyser(self): """Return analyser""" return self._analyser @property def results(self): """Return results""" return self._results @property def results_order(self): """Return results_order""" return self._results_order @property def file_path_ref(self): """Get file path reference""" return self._file_path_ref @file_path_ref.setter def file_path_ref(self, file_path_ref): """Set file path reference""" self._file_path_ref = file_path_ref @property def outpath(self): """Return outpath""" return self._outpath @outpath.setter def outpath(self, new_outpath): """Overwrite outpath""" self._outpath = new_outpath @property def substitutesets(self): """Return substitutesets""" return self._substitutesets @property def workpackages(self): """Return workpackages""" return self._workpackages def add_tags(self, other_tags): if other_tags is not None: self._tags = self._tags.union(set(other_tags)) def workpackage_by_id(self, wp_id): """Search and return a benchmark workpackage by its wp_id""" for stepname in self._workpackages: for workpackage in self._workpackages[stepname]: if workpackage.id == wp_id: return workpackage return None def remove_workpackage(self, workpackage_to_delete): """Remove a specifc workpackage""" stepname = workpackage_to_delete.step.name if stepname in self._workpackages and \ workpackage_to_delete in self._workpackages[stepname]: self._workpackages[stepname].remove(workpackage_to_delete) @property def work_stat(self): """Return work queue""" return self._work_stat @property def filesets(self): """Return filesets""" return self._filesets def delete_bench_dir(self): """Delete all data inside benchmark directory""" if os.path.exists(self.bench_dir): shutil.rmtree(self.bench_dir, ignore_errors=True) @property def steps(self): """Return steps""" return self._steps @property def workpackage_status(self): """Retun workpackage information dict""" result_dict = dict() for stepname in self._workpackages: result_dict[stepname] = {"all": 0, "open": 0, "wait": 0, "error": 0, "done": 0} for workpackage in self._workpackages[stepname]: result_dict[stepname]["all"] += 1 if workpackage.done: result_dict[stepname]["done"] += 1 elif workpackage.error: result_dict[stepname]["error"] += 1 elif workpackage.started: result_dict[stepname]["wait"] += 1 else: result_dict[stepname]["open"] += 1 return result_dict @property def benchmark_status(self): """Retun global workpackage information dict""" result_dict = {"all": 0, "open": 0, "wait": 0, "error": 0, "done": 0} for status in self.workpackage_status.values(): result_dict["all"] += status["all"] result_dict["open"] += status["open"] result_dict["wait"] += status["wait"] result_dict["error"] += status["error"] result_dict["done"] += status["done"] return result_dict @property def id(self): """Return benchmark id""" return self._id @id.setter def id(self, new_id): """Set new benchmark id""" self._id = new_id def get_jube_parameterset(self): """Return parameterset which contains benchmark related information""" parameterset = jube2.parameter.Parameterset() # benchmark id parameterset.add_parameter( jube2.parameter.Parameter. create_parameter( "jube_benchmark_id", str(self._id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # benchmark id with padding parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_padid", jube2.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) # benchmark name parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_name", self._name, update_mode=jube2.parameter.JUBE_MODE)) # benchmark home parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_home", os.path.abspath(self._file_path_ref), update_mode=jube2.parameter.JUBE_MODE)) # benchmark rundir parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_rundir", os.path.abspath(self.bench_dir), update_mode=jube2.parameter.JUBE_MODE)) timestamps = jube2.util.util.read_timestamps( os.path.join(self.bench_dir, jube2.conf.TIMESTAMPS_INFO)) # benchmark start parameterset.add_parameter( jube2.parameter.Parameter.create_parameter( "jube_benchmark_start", timestamps.get("start", "").replace(" ", "T"), update_mode=jube2.parameter.JUBE_MODE)) return parameterset def etree_repr(self, new_cwd=None): """Return etree object representation""" benchmark_etree = ET.Element("benchmark") if len(self._comment) > 0: comment_element = ET.SubElement(benchmark_etree, "comment") comment_element.text = self._comment benchmark_etree.attrib["name"] = self._name # Modify file_path_ref and outpath to be relativly correct towards # new configuration file position if new_cwd is not None: benchmark_etree.attrib["file_path_ref"] = \ os.path.relpath(self._file_path_ref, new_cwd) if not os.path.isabs(self._outpath): benchmark_etree.attrib["outpath"] = \ os.path.relpath(self._outpath, new_cwd) else: benchmark_etree.attrib["outpath"] = self._outpath for parameterset in self._parametersets.values(): benchmark_etree.append(parameterset.etree_repr()) for substituteset in self._substitutesets.values(): benchmark_etree.append(substituteset.etree_repr()) for fileset in self._filesets.values(): benchmark_etree.append(fileset.etree_repr()) for patternset in self._patternsets.values(): benchmark_etree.append(patternset.etree_repr()) for step in self._steps.values(): benchmark_etree.append(step.etree_repr()) for analyser in self._analyser.values(): benchmark_etree.append(analyser.etree_repr()) for result_name in self._results_order: result = self._results[result_name] benchmark_etree.append(result.etree_repr()) return benchmark_etree def __repr__(self): return pprint.pformat(self.__dict__) def _create_initial_workpackages(self): """Create initial workpackages of current benchmark and create graph structure.""" self._workpackages = dict() self._work_stat = jube2.util.util.WorkStat() # Create workpackage storage for step_name in self._steps: self._workpackages[step_name] = list() # Create initial workpackages for step in self._steps.values(): if len(step.depend) == 0: new_workpackages = \ self._create_new_workpackages_with_parents(step) self._workpackages[step.name] += new_workpackages for workpackage in new_workpackages: workpackage.queued = True self._work_stat.put(workpackage) def analyse(self, show_info=True, specific_analyser_name=None): """Run analyser""" if show_info: LOGGER.info(">>> Start analyse") if specific_analyser_name is not None and \ specific_analyser_name in self._analyser: self._analyser[specific_analyser_name].analyse() else: for analyser in self._analyser.values(): analyser.analyse() if ((not jube2.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_analyse_data(os.path.join(self.bench_dir, jube2.conf.ANALYSE_FILENAME)) if show_info: LOGGER.info(">>> Analyse finished") def create_result(self, only=None, show=False, data_list=None, style=None, select=None, exclude=None): """Show benchmark result""" if only is None: only = [result_name for result_name in self._results] if data_list is None: data_list = list() for result_name in self._results_order: result = self._results[result_name] if result.name in only: result_data = result.create_result_data(style, select, exclude) if result.result_dir is None: result_dir = os.path.join(self.bench_dir, jube2.conf.RESULT_DIRNAME) else: result_dir = result.result_dir result_dir = os.path.expanduser(result_dir) result_dir = os.path.expandvars(result_dir) result_dir = jube2.util.util.id_dir( os.path.join(self.file_path_ref, result_dir), self.id) if (not os.path.exists(result_dir)) and \ (not jube2.conf.DEBUG_MODE): try: os.makedirs(result_dir) except OSError: pass if ((not jube2.conf.DEBUG_MODE) and (os.path.exists(result_dir)) and (os.access(result_dir, os.W_OK))): filename = os.path.join(result_dir, "{0}.dat".format(result.name)) else: filename = None result_data.create_result(show=show, filename=filename) if result_data in data_list: data_list[data_list.index(result_data)].add_result_data( result_data) else: data_list.append(result_data) return data_list def update_analyse_and_result(self, new_patternsets, new_analyser, new_results, new_results_order, new_cwd): """Update analyser and result data""" if os.path.exists(self.bench_dir): LOGGER.debug("Update analyse and result data") self._patternsets = new_patternsets old_analyser = self._analyser self._analyser = new_analyser self._results = new_results self._results_order = new_results_order for analyser in self._analyser.values(): if analyser.name in old_analyser: analyser.analyse_result = \ old_analyser[analyser.name].analyse_result analyser.benchmark = self for result in self._results.values(): result.benchmark = self # change result dir position relative to cwd if (result.result_dir is not None) and \ (new_cwd is not None) and \ (not os.path.isabs(result.result_dir)): result.result_dir = \ os.path.join(new_cwd, result.result_dir) if ((not jube2.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_benchmark_configuration( os.path.join(self.bench_dir, jube2.conf.CONFIGURATION_FILENAME), outpath="..") def write_analyse_data(self, filename): """All analyse data will be written to given file using xml representation""" # Create root-tag and append analyser analyse_etree = ET.Element("analyse") for analyser_name in self._analyser: analyser_etree = ET.SubElement(analyse_etree, "analyser") analyser_etree.attrib["name"] = analyser_name for etree in self._analyser[analyser_name].analyse_etree_repr(): analyser_etree.append(etree) xml = jube2.util.output.element_tree_tostring( analyse_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode("UTF-8")) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def _create_new_workpackages_for_workpackage(self, workpackage): """Create and return new workpackages if given workpackage was finished.""" all_new_workpackages = list() if not workpackage.done or len(workpackage.children) > 0: return all_new_workpackages LOGGER.debug(("Create new workpackages for workpackage" " {0}({1})").format( workpackage.step.name, workpackage.id)) # Search for dependent steps dependent_steps = [step for step in self._steps.values() if workpackage.step.name in step.depend] # Search for possible workpackage parents for dependent_step in dependent_steps: parent_workpackages = [[ parent_workpackage for parent_workpackage in self._workpackages[step_name] if parent_workpackage.done] for step_name in dependent_step.depend if (step_name in self._workpackages) and (step_name != workpackage.step.name)] parent_workpackages.append([workpackage]) # Create all possible parent combinations workpackage_combinations = \ [iterator for iterator in itertools.product(*parent_workpackages)] possible_combination = len(workpackage_combinations) for workpackage_combination in workpackage_combinations: new_workpackages = self._create_new_workpackages_with_parents( dependent_step, workpackage_combination) if len(new_workpackages) > 0: possible_combination -= 1 # Create links: parent workpackages -> new children for new_workpackage in new_workpackages: for parent in workpackage_combination: parent.add_children(new_workpackage) self._workpackages[dependent_step.name] += new_workpackages all_new_workpackages += new_workpackages if possible_combination > 0: LOGGER.debug((" {0} workpackages combinations were skipped" " while checking possible parent combinations" " for step {1}").format(possible_combination, dependent_step.name)) LOGGER.debug(" {0} new workpackages created".format( len(all_new_workpackages))) return all_new_workpackages def _create_new_workpackages_with_parents(self, step, parent_workpackages=None): """Create workpackages with given parent combination""" if parent_workpackages is None: parent_workpackages = list() # Combine and check parent parametersets parameterset = jube2.parameter.Parameterset() incompatible_parameter_names = set() for parent_workpackage in parent_workpackages: # Check weather parameter combination is possible or not. # JUBE Parameter can be ignored incompatible_parameter_names = incompatible_parameter_names.union( parameterset.get_incompatible_parameter( parent_workpackage.parameterset, update_mode=jube2.parameter.JUBE_MODE)) parameterset.add_parameterset( parent_workpackage.parameterset) # Sort parent workpackges after total iteration number and name sorted_parents = list(parent_workpackages) sorted_parents.sort(key=lambda x: x.step.name) sorted_parents.sort(key=lambda x: x.step.iterations) iteration_base = 0 for i, parent in enumerate(sorted_parents): if i == 0: iteration_base = parent.iteration else: iteration_base = \ parent.step.iterations * iteration_base + parent.iteration parameterset.remove_jube_parameter() # Create new workpackages new_workpackages = step.create_workpackages( self, parameterset, iteration_base=iteration_base, parents=parent_workpackages, incompatible_parameters=incompatible_parameter_names) # Update iteration sibling connections if len(parent_workpackages) > 0 and len(new_workpackages) > 0: for sibling in parent_workpackages[0].iteration_siblings: if sibling != parent_workpackages[0]: for child in sibling.children: for workpackage in new_workpackages: if workpackage.parameterset.is_compatible( child.parameterset, update_mode=jube2.parameter.JUBE_MODE): workpackage.iteration_siblings.add(child) child.iteration_siblings.add(workpackage) return new_workpackages def new_run(self): """Create workpackage structure and run benchmark""" # Check benchmark consistency LOGGER.debug("Start consistency check") jube2.util.util.consistency_check(self) # Create benchmark directory LOGGER.debug("Create benchmark directory") self._create_bench_dir() # Change logfile jube2.log.change_logfile_name(os.path.join( self.bench_dir, jube2.conf.LOGFILE_RUN_NAME)) # Move parse logfile into benchmark folder if os.path.isfile(os.path.join(self._file_path_ref, jube2.conf.DEFAULT_LOGFILE_NAME)): shutil.move(os.path.join(self._file_path_ref, jube2.conf.DEFAULT_LOGFILE_NAME), os.path.join(self.bench_dir, jube2.conf.LOGFILE_PARSE_NAME)) # Reset Workpackage counter jube2.workpackage.Workpackage.id_counter = 0 # Create initial workpackages LOGGER.debug("Create initial workpackages") self._create_initial_workpackages() # Store workpackage information LOGGER.debug("Store initial workpackage information") self.write_workpackage_information( os.path.join(self.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) LOGGER.debug("Start benchmark run") self.run() def run(self): """Run benchmark""" title = "benchmark: {0}".format(self._name) title += "\nid: {0}".format(self._id) if jube2.conf.DEBUG_MODE: title += " ---DEBUG_MODE---" title += "\n\n{0}".format(self._comment) infostr = jube2.util.output.text_boxed(title) LOGGER.info(infostr) if not jube2.conf.HIDE_ANIMATIONS: print("\nRunning workpackages (#=done, 0=wait, E=error):") status = self.benchmark_status jube2.util.output.print_loading_bar( status["done"], status["all"], status["wait"], status["error"]) # Handle all workpackages in given order while not self._work_stat.empty(): workpackage = self._work_stat.get() run_parallel = False def collect_result(val): """used collect return values from pool.apply_async""" # run postprocessing of each wp for i, wp in enumerate(self._workpackages[val["step_name"]]): if wp.id == val["id"]: if(len(val) == 2): # workpackage is done or its execution was erroneous pass else: # update corresponding wp in self._workpackage with modified wp wp.env = val["env"] # restore the parameters containing a method of a class, # which needed to be deleted within the multiprocess # execution to avoid excessive memory usage for p in wp._parameterset.all_parameters: if(p.search_method(propertyString="eval_helper", recursiveProperty="based_on")): val["parameterset"].add_parameter(p) wp.parameterset = val["parameterset"] wp.cycle = val["cycle"] self.wp_post_run_config(wp) break def log_e(e): """used to print error_callback from pool.apply_async""" print(e) # TODO # writeXML position(y) - replace by database # TODO END if not workpackage.done: # execute wps in parallel which have the same name if workpackage.step.procs > 1: run_parallel = True procs = workpackage.step.procs name = workpackage.step.name pool = mp.Pool(processes=procs) # save current logfile name to restore logs in the right logfile current_logfile_name = jube2.log.LOGFILE_NAME # add wps to the parallel pool as long as they have the same name while True: pool.apply_async(workpackage.run, args=('p',), callback=collect_result, error_callback=log_e) if not self._work_stat.empty(): workpackage = self._work_stat.get() # push back as first element of _work_stat and # terminate parallel loop if workpackage.step.name != name: self._work_stat.push_back(workpackage) break else: break pool.close() pool.join() else: workpackage.run() if run_parallel == True: # merge parallel run log files into the main run log file and # delete the parallel logs log_fname = jube2.log.LOGFILE_NAME.split('/')[-1] filenames = [file for file in os.listdir(self.bench_dir) if file.startswith(log_fname.split('.')[0]) and file != log_fname] filenames.sort(key=lambda o: int(re.split('_|\.', o)[1])) with open(current_logfile_name, 'a') as outfile: for fname in filenames: with open(os.path.join(self.bench_dir, fname), 'r') as infile: contents = infile.read() outfile.write(contents) os.remove(os.path.join(self.bench_dir, fname)) run_parallel = False else: self.wp_post_run_config(workpackage) # Store workpackage information self.write_workpackage_information( os.path.join(self.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) print("\n") status_data = [("stepname", "all", "open", "wait", "error", "done")] status_data += [(stepname, str(_status["all"]), str(_status["open"]), str(_status["wait"]), str(_status["error"]), str(_status["done"])) for stepname, _status in self.workpackage_status.items()] LOGGER.info(jube2.util.output.text_table( status_data, use_header_line=True, indent=2)) LOGGER.info("\n>>>> Benchmark information and " + "further useful commands:") LOGGER.info(">>>> id: {0}".format(self._id)) LOGGER.info(">>>> handle: {0}".format(self._outpath)) LOGGER.info(">>>> dir: {0}".format(self.bench_dir)) status = self.benchmark_status if status["all"] != status["done"]: LOGGER.info((">>>> continue: jube continue {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> analyse: jube analyse {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> result: jube result {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> info: jube info {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> log: jube log {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info(jube2.util.output.text_line() + "\n") def wp_post_run_config(self, workpackage): """additional processing of workpackage: - update status bar - build up queue after restart """ self._create_new_workpackages_for_workpackage(workpackage) # Update queues (move waiting workpackages to work queue # if possible) self._work_stat.update_queues(workpackage) #Update workpackage status for jube parameter workpackage.update_status() if not jube2.conf.HIDE_ANIMATIONS: status = self.benchmark_status jube2.util.output.print_loading_bar( status["done"], status["all"], status["wait"], status["error"]) workpackage.queued = False for mode in ("only_started", "all"): for child in workpackage.children: all_done = True for parent in child.parents: all_done = all_done and parent.done if all_done: if (mode == "only_started" and child.started) or \ (mode == "all" and (not child.queued)): child.queued = True self._work_stat.put(child) def _create_bench_dir(self): """Create the directory for a benchmark.""" # Get group_id if available (given by JUBE_GROUP_NAME) group_id = jube2.util.util.check_and_get_group_id() # Check if outpath exists if not (os.path.exists(self._outpath) and os.path.isdir(self._outpath)): os.makedirs(self._outpath) if group_id is not None: os.chown(self._outpath, os.getuid(), group_id) # Generate unique ID in outpath if self._id < 0: self._id = jube2.util.util.get_current_id(self._outpath) + 1 if os.path.exists(self.bench_dir): raise RuntimeError("Benchmark directory \"{0}\" already exists" .format(self.bench_dir)) os.makedirs(self.bench_dir) # If JUBE_GROUP_NAME is given, set GID-Bit and change group if group_id is not None: os.chown(self.bench_dir, os.getuid(), group_id) os.chmod(self.bench_dir, os.stat(self.bench_dir).st_mode | stat.S_ISGID) self.write_benchmark_configuration( os.path.join(self.bench_dir, jube2.conf.CONFIGURATION_FILENAME), outpath="..") jube2.util.util.update_timestamps(os.path.join( self.bench_dir, jube2.conf.TIMESTAMPS_INFO), "start", "change") def write_benchmark_configuration(self, filename, outpath=None): """The current benchmark configuration will be written to given file using xml representation""" # Create root-tag and append single benchmark benchmarks_etree = ET.Element("jube") benchmarks_etree.attrib["version"] = jube2.conf.JUBE_VERSION # Store tag information if len(self._tags) > 0: selection_etree = ET.SubElement(benchmarks_etree, "selection") for tag in self._tags: tag_etree = ET.SubElement(selection_etree, "tag") tag_etree.text = tag benchmark_etree = self.etree_repr(new_cwd=self.bench_dir) if outpath is not None: benchmark_etree.attrib["outpath"] = outpath benchmarks_etree.append(benchmark_etree) xml = jube2.util.output.element_tree_tostring( benchmarks_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode('UTF-8')) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def reset_all_workpackages(self): """Reset workpackage state""" for workpackages in self._workpackages.values(): for workpackage in workpackages: workpackage.done = False def write_workpackage_information(self, filename): """All workpackage information will be written to given file using xml representation""" # Create root-tag and append workpackages workpackages_etree = ET.Element("workpackages") for workpackages in self._workpackages.values(): for workpackage in workpackages: workpackages_etree.append(workpackage.etree_repr()) xml = jube2.util.output.element_tree_tostring( workpackages_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode("UTF-8")) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def set_workpackage_information(self, workpackages, work_stat): """Set new workpackage information""" self._workpackages = workpackages self._work_stat = work_stat @property def bench_dir(self): """Return benchmark directory""" return jube2.util.util.id_dir(self._outpath, self._id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/completion.py0000664000174700017470000000637514603772010020175 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Shell Completions""" from __future__ import (print_function, unicode_literals, division) import jube2.main # This is formatted once. BASH_CASE_TEMPLATE = """\ "{command}") COMPREPLY=( $(compgen -W "{opts}" -- ${{cur}}) ) return 0 ;; """ # This is formatted once. BASH_SCRIPT_TEMPLATE = """ _{command_name} () {{ local cur prev words cword comm subparsers subcom iter COMPREPLY=() words=(${{COMP_WORDS[@]}}) cword=COMP_CWORD comm=${{words[0]}} cur="${{words[cword]}}" prev="${{words[cword-1]}}" subcom="${{words[0]}}" for iter in ${{words[@]:1}}; do if [[ $iter != -* ]] && [[ " {all_subcoms} " == *" $iter "* ]]; then subcom=$iter break fi done subparsers="{subparser}" if [[ ${{cur}} == -* ]] ; then case "${{subcom}}" in {cases_sub} *) esac elif [[ ${{subcom}} == "$comm" ]] ; then COMPREPLY=( $(compgen -W "${{subparsers}}" -- ${{cur}}) ) fi }} && complete -o bashdefault -o default -F _{command_name} {command_name} """ def complete_function_bash(args): """Print completion function for bash.""" subparser = jube2.main.gen_subparser_conf() all_sub_names = " ".join(sorted(subparser)) parser = sorted([opt for opts, kwargs in jube2.main.gen_parser_conf() for opt in opts if opt.startswith("--")]) command_name = args.command_name[0] complete_options = dict() # Iterate over all subparsers for sub_name, sub in sorted(subparser.items()): if "arguments" not in sub: continue # Iterate over all their options tmp_list = [argument for key in sub["arguments"] for argument in key if argument.startswith("--")] complete_options[sub_name] = " ".join(tmp_list) cases_sub = "".join(BASH_CASE_TEMPLATE.format(command=command, opts=opts) for command, opts in sorted(complete_options.items())) cases_sub += BASH_CASE_TEMPLATE.format(command=command_name, opts=" ".join(parser)) subparser_str = " ".join(sorted(subparser.keys())) script = BASH_SCRIPT_TEMPLATE.format( subparser=subparser_str, cases_sub=cases_sub, command_name=command_name, all_subcoms=all_sub_names) print(script) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/conf.py0000664000174700017470000000465114603772010016744 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Configuration""" from __future__ import (print_function, unicode_literals, division) # general JUBE_VERSION = "2.6.2" ALLOWED_SCRIPTTYPES = set(["python", "perl", "shell"]) ALLOWED_ADVANCED_MODETYPES = set(["tag", "env"]) ALLOWED_MODETYPES = set(["text"]).union(ALLOWED_SCRIPTTYPES).union( ALLOWED_ADVANCED_MODETYPES) DEBUG_MODE = False VERBOSE_LEVEL = 0 UPDATE_VERSION_URL = "http://apps.fz-juelich.de/jsc/jube/jube2/version" UPDATE_URL = "http://apps.fz-juelich.de/jsc/jube/jube2/download.php" STANDARD_SHELL = "/bin/sh" EXIT_ON_ERROR = False # input/output DEFAULT_SEPARATOR = "," ZERO_FILL_DEFAULT = 6 DEFAULT_WIDTH = 70 MAX_TABLE_CELL_WIDTH = 40 HIDE_ANIMATIONS = False VERBOSE_STDOUT_READ_CHUNK_SIZE = 50 VERBOSE_STDOUT_POLL_SLEEP = 0.05 SYSLOG_FMT_STRING = "jube[%(process)s]: %(message)s" PREPROCESS_MAX_ITERATION = 10 # filenames WORKPACKAGE_DONE_FILENAME = "done" WORKPACKAGE_ERROR_FILENAME = "error" DO_LOG_FILENAME = "do_log" CONFIGURATION_FILENAME = "configuration.xml" WORKPACKAGES_FILENAME = "workpackages.xml" ANALYSE_FILENAME = "analyse.xml" RESULT_DIRNAME = "result" ENVIRONMENT_INFO = "jube_environment_information.dat" TIMESTAMPS_INFO = "timestamps" # logging DEFAULT_LOGFILE_NAME = "jube-parse.log" LOGFILE_DEBUG_NAME = "jube-debug.log" LOGFILE_DEBUG_MODE = "w" LOGFILE_RUN_NAME = "run.log" LOGFILE_CONTINUE_NAME = "continue.log" LOGFILE_ANALYSE_NAME = "analyse.log" LOGFILE_PARSE_NAME = "parse.log" LOGFILE_RESULT_NAME = "result.log" LOG_CONSOLE_FORMAT = "%(message)s" LOG_FILE_FORMAT = "[%(asctime)s]:%(levelname)s: %(message)s" DEFAULT_LOGGING_MODE = "default" # other ERROR_MSG_LINES = 5 MAX_RECURSIVE_SUB = 5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/fileset.py0000664000174700017470000002530514603772010017451 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Fileset related classes""" from __future__ import (print_function, unicode_literals, division) import os import shutil import xml.etree.ElementTree as ET import jube2.util.util import jube2.conf import jube2.step import jube2.log import glob LOGGER = jube2.log.get_logger(__name__) class Fileset(list): """Container for file copy, link and prepare operations""" def __init__(self, name): list.__init__(self) self._name = name @property def name(self): """Return fileset name""" return self._name def etree_repr(self): """Return etree object representation""" fileset_etree = ET.Element("fileset") fileset_etree.attrib["name"] = self._name for file_handle in self: fileset_etree.append(file_handle.etree_repr()) return fileset_etree def create(self, work_dir, parameter_dict, alt_work_dir=None, environment=None, file_path_ref=""): """Copy/load/prepare all files in fileset""" for file_handle in self: if type(file_handle) is Prepare: file_handle.execute( parameter_dict=parameter_dict, work_dir=alt_work_dir if alt_work_dir is not None else work_dir, environment=environment) else: file_handle.create( work_dir=work_dir, parameter_dict=parameter_dict, alt_work_dir=alt_work_dir, file_path_ref=file_path_ref, environment=environment) class File(object): """Generic file access""" def __init__(self, path, name=None, is_internal_ref=False, active="true", source_dir="", target_dir=""): self._path = path self._source_dir = source_dir self._target_dir = target_dir self._name = name self._file_path_ref = "" self._active = active self._is_internal_ref = is_internal_ref def create(self, work_dir, parameter_dict, alt_work_dir=None, file_path_ref="", environment=None): """Create file access""" # Check active status active = jube2.util.util.eval_bool(jube2.util.util.substitution( self._active, parameter_dict)) if not active: return pathname = jube2.util.util.substitution(self._path, parameter_dict) pathname = os.path.expanduser(pathname) source_dir = jube2.util.util.substitution(self._source_dir, parameter_dict) source_dir = os.path.expanduser(source_dir) target_dir = jube2.util.util.substitution(self._target_dir, parameter_dict) target_dir = os.path.expanduser(target_dir) if environment is not None: pathname = jube2.util.util.substitution(pathname, environment) source_dir = jube2.util.util.substitution(source_dir, environment) target_dir = jube2.util.util.substitution(target_dir, environment) else: pathname = os.path.expandvars(pathname) source_dir = os.path.expandvars(source_dir) target_dir = os.path.expandvars(target_dir) # Add source prefix directory if needed pathname = os.path.join(source_dir, pathname) if self._is_internal_ref: pathname = os.path.join(work_dir, pathname) else: pathname = os.path.join(self._file_path_ref, pathname) pathname = os.path.join(file_path_ref, pathname) pathname = os.path.normpath(pathname) if self._name is None: name = os.path.basename(pathname) else: name = jube2.util.util.substitution(self._name, parameter_dict) name = os.path.expanduser(name) if environment is not None: name = jube2.util.util.substitution(name, environment) else: name = os.path.expandvars(name) if alt_work_dir is not None: work_dir = alt_work_dir # Shell expansion pathes = glob.glob(pathname) if (len(pathes) == 0) and (not jube2.conf.DEBUG_MODE): raise RuntimeError("no files found using \"{0}\"" .format(pathname)) for path in pathes: # When using shell extensions, alternative filenames are not # allowed for multiple matches. if (len(pathes) > 1) or ((pathname != path) and (name == os.path.basename(pathname))): name = os.path.basename(path) # Add target prefix directory if needed name = os.path.join(target_dir, name) new_file_path = os.path.join(work_dir, name) # Create target_dir if needed if (len(os.path.dirname(new_file_path)) > 0 and not os.path.exists(os.path.dirname(new_file_path)) and not jube2.conf.DEBUG_MODE): os.makedirs(os.path.dirname(new_file_path)) self.create_action(path, name, new_file_path) def create_action(self, path, name, new_file_path): """File access type specific creation""" raise NotImplementedError() def etree_repr(self): """Return etree object representation""" raise NotImplementedError() @property def path(self): """Return filepath""" return self._path @property def file_path_ref(self): """Get file path reference""" return self._file_path_ref @file_path_ref.setter def file_path_ref(self, file_path_ref): """Set file path reference""" self._file_path_ref = file_path_ref @property def is_internal_ref(self): """Return path is internal ref""" return self._is_internal_ref def __repr__(self): return self._path class Link(File): """A link to a given path. Which can be used inside steps.""" def create_action(self, path, name, new_file_path): """Create link to file in work_dir""" # Manipulate target_path if a new relative name path was selected if os.path.isabs(path): target_path = path else: target_path = os.path.relpath(path, os.path.dirname(new_file_path)) LOGGER.debug(" link \"{0}\" <- \"{1}\"".format(target_path, name)) if not jube2.conf.DEBUG_MODE and not os.path.exists(new_file_path): os.symlink(target_path, new_file_path) def etree_repr(self): """Return etree object representation""" link_etree = ET.Element("link") link_etree.text = self._path if self._name is not None: link_etree.attrib["name"] = self._name if self._active != "true": link_etree.attrib["active"] = self._active if self._source_dir != "": link_etree.attrib["source_dir"] = self._source_dir if self._target_dir != "": link_etree.attrib["target_dir"] = self._target_dir if self._is_internal_ref: link_etree.attrib["rel_path_ref"] = "internal" if self._file_path_ref != "": link_etree.attrib["file_path_ref"] = self._file_path_ref return link_etree class Copy(File): """A file or directory given by path. Which can be copied to the work_dir inside steps. """ def create_action(self, path, name, new_file_path): """Copy file/directory to work_dir""" LOGGER.debug(" copy \"{0}\" -> \"{1}\"".format(path, name)) if not jube2.conf.DEBUG_MODE and not os.path.exists(new_file_path): if os.path.isdir(path): shutil.copytree(path, new_file_path, symlinks=True) else: shutil.copy2(path, new_file_path) def etree_repr(self): """Return etree object representation""" copy_etree = ET.Element("copy") copy_etree.text = self._path if self._name is not None: copy_etree.attrib["name"] = self._name if self._active != "true": copy_etree.attrib["active"] = self._active if self._source_dir != "": copy_etree.attrib["source_dir"] = self._source_dir if self._target_dir != "": copy_etree.attrib["target_dir"] = self._target_dir if self._is_internal_ref: copy_etree.attrib["rel_path_ref"] = "internal" if self._file_path_ref != "": copy_etree.attrib["file_path_ref"] = self._file_path_ref return copy_etree class Prepare(jube2.step.Operation): """Prepare the workpackage work directory""" def __init__(self, cmd, stdout_filename=None, stderr_filename=None, work_dir=None, active="true"): jube2.step.Operation.__init__(self, do=cmd, stdout_filename=stdout_filename, stderr_filename=stderr_filename, active=active, work_dir=work_dir) def execute(self, parameter_dict, work_dir, only_check_pending=False, environment=None): """Execute the prepare command""" jube2.step.Operation.execute( self, parameter_dict=parameter_dict, work_dir=work_dir, only_check_pending=only_check_pending, environment=environment) def etree_repr(self): """Return etree object representation""" do_etree = ET.Element("prepare") do_etree.text = self._do if self._stdout_filename is not None: do_etree.attrib["stdout"] = self._stdout_filename if self._stderr_filename is not None: do_etree.attrib["stderr"] = self._stderr_filename if self._active != "true": do_etree.attrib["active"] = self._active if self._work_dir is not None: do_etree.attrib["work_dir"] = self._work_dir return do_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/help.py0000664000174700017470000000316414603772010016745 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """User help""" from __future__ import (print_function, unicode_literals, division) import jube2 import os import re HELP = dict() def load_help(): """Load additional documentation out of help file and add these data to global help dictionary.""" path = os.path.join(jube2.__path__[0], "help.txt") help_file = open(path, "r") group = None # skip header lines i = 0 while i < 4: help_file.readline() i += 1 for line in help_file: # search for new abstract inside of help file matcher = re.match(r"^(\S+)s*$", line) if matcher is not None: group = matcher.group(1) HELP[group] = "" else: if (len(line) > 0) and (group is not None): HELP[group] += line[0] + line[3:] help_file.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/help.txt0000664000174700017470000011122414603772010017131 0ustar00gitlab-runnergitlab-runnerGlossary ******** analyse Analyse an existing benchmark. The analyser will scan through all files given inside the configuration by using the given patternsets. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. analyser_tag The analyser describe the steps and files which should be scanned using a set of pattern. ... ... ... ... * you can use different patternsets to analyse a set of files * only patternsets are usable * using patternsets "set1,set2" is the same as "set1set2" * the from-attribute is optional and can be used to specify an external set source * any name must be unique, it is not allowed to reuse a set * the step-attribute contains an existing stepname * each file using each workpackage will be scanned seperatly * the "use" argument inside the "" tag is optional and can be used to specify a file specific patternset; * the global "" and this local use will be combined and evaluated at the same time * a "from" subargument is not possible in this local "use" * "reduce" is optional (default: "true" ) * "true" : Combine result lines if iteration-option is used * "false" : Create single line for each iteration benchmark_tag The main benchmark definition ... * container for all benchmark information * benchmark-name must be unique inside input file * "outpath" contains the path to the root folder for benchmark runs * multiple benchmarks can use the same folder * every benchmark and every (new) run will create a new folder (named by an unique benchmark id) inside this given "outpath" * the path will be relative to input file location check_tags_tag Specify combination of tags that must be set. ... * The combination is set using boolean algebra. * For the logical operation 'conjunction', i.e. 'and', the sign "+" is used. Example: "tag1 + tag2" means that both tags must be set. * For the logical operation 'disjunction', i.e. 'or', the character "|" is used. Example: "tag1 | tag2" means that one of the two or both tags must be set. * In addition, the character "!" can be used for the logical operation 'negation', i.e. 'not'. Example: "!tag1" means that the tag "tag1" must not be set. column_tag A line within a ASCII result table. The ""-tag can contain the name of a pattern or the name of a parameter. ... * "colw" is optional: column width * "title" is optional: column title * "format" can contain a C like format string: e.g. "format=".2f"" comment Add or manipulate the comment string. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. comment_tag Add a benchmark specific comment. These comment will be stored inside the benchmark directory. ... complete Generate shell completion. continue Continue an existing benchmark. Not finished steps will be continued, if they are leaving pending mode. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. copy_tag A copy can be used to copy a file or directory from your normal filesytem to your sandbox work directory. ... * "source_dir" is optional, will be used as a prefix for the source filenames * "target_dir" is optional, will be used as a prefix for the target filenames * "name" is optional, it can be used to rename the file inside your work directory (will be ignored if you use shell extensions in your pathname) * "rel_path_ref" is optional * "external" or "internal" can be chosen, default: external * "external": rel.-paths based on position of xml-file * "internal": rel.-paths based on current work directory (e.g. to link files of another step) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * each copy-tag can contain a list of filenames (or directories), separated by ",", the default separator can be changed by using the "separator" attribute * if "name" is present, the lists must have the same length * you can copy all files inside a directory by using "directory/*" * this cannot be mixed using "name" * in the execution step the given files or directories will be copied database_tag Create sqlite3 database ... ... * "name": name of the table in the database * "" must contain an single parameter or pattern name * Unlike the result table, the unit attribute of a parameter or pattern is not taken into account. * "primekeys" is optional: can contain a list of parameter or pattern names (separated by ","). Given parameters or patterns will be used as primary keys of the database table. All primekeys have to be listed as a "" as well. Modification of primary keys of an existing table is not supported. If no primekeys are set then each *jube result* will add new rows to the database. Otherwise rows with matching primekeys will be updated. * "file" is optional. The given value should hold the full path to the database file. If the file including the path does not exists it will be created. Absolute and relative paths are supported. * "filter" is optional. It can contain a bool expression to show only specific result entries. directory_structure * every (new) benchmark run will create its own directory structure * every single workpackage will create its own directory structure * user can add files (or links) to the workpackage dir, but the real position in filesystem will be seen as a blackbox * general directory structure: benchmark_runs (given by "outpath" in xml-file) | +- 000000 (determined through benchmark-id) | +- 000000_compile (step: just an example, can be arbitrary chosen) | +- work (user environment) +- done (workpackage finished information file) +- ... (more jube internal information files) +- 000001_execute | +- work | +- compile -> ../../000000_compile/work (automatic generated link for depending step) +- wp_done_00 (single "do" finished, but not the whole workpackage) +- ... +- 000002_execute +- result (result data) +- configuration.xml (benchmark configuration information file) +- workpackages.xml (workpackage graph information file) +- analyse.xml (analyse data) +- 000001 (determined through benchmark-id) | +- 000000_compile (step: just an example, can be arbitrary chosen) +- 000001_execute +- 000002_postprocessing do_tag A do contain a executable *Shell* operation. ... ... ... ... ... * "do" can contain any *Shell*-syntax-snippet (*parameter* will be replaced "... $nameofparameter ...") * "stdout"- and "stderr"-filename are optional (default: "stdout" and "stderr") * "work_dir" is optional, it can be used to change the work directory of this single command (relativly seen towards the original work directory) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * "done_file"-filename and "error_file" are optional * by using "done_file" the user can mark async-steps. The operation will stop until the script will create the named file inside the work directory. * by using "error_file" the operation will produce a error if the named file can be found inside the work directory. This feature can be used together with the "done_file" to signalise broken async-steps. * "break_file"-filename is optional * by using "break_file" the user can stop further cycle runs. the current step will be directly marked with finalized and further "" will be ignored. * "shared="true"" * can be used inside a step using a shared folder * cmd will be **executed inside the shared folder** * cmd will run once (synchronize all workpackages) * "$jube_wp_..." - parameter cannot be used inside the shared command fileset_tag A fileset is a container to store a bundle of links and copy commands. ... ... ... ... * init_with is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a fileset using the given name, all link and copy will be copied to the local set * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * link and copy can be mixed within one fileset (or left) * filesets can be used inside the step-command general_structure_xml ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... general_structure_yaml # optional additional include paths include-path: ... # optional benchmark selection selection: only: ... not: ... tag: ... # optional must-have tag specification check_tags: ... # global sets parameterset: ... substitutionset: ... fileset: ... patternset: ... benchmark: # can be skipped if only a single benchmark is handled - name: ... outpath: ... # optional benchmark comment comment: ... # local sets parameterset: ... substitutionset: ... fileset: ... patternset: ... # commands step: ... analyser: ... result: ... include-path_tag Add some include paths where to search for include files. ... ... * the additional path will be scanned for include files include_tag Include *XML*-data from an external file. * "" can be used to include an external *XML*-structure into the current file * can be used at every position (inside the ""-tag) * path is optional and can be used to give an alternative xml-path inside the include-file (default: root-node) info Show info for the given benchmark directory, a given benchmark or a specific step. If benchmark directory is missing, current directory will be used. iofile_tag A iofile declare the name (and path) of a file used for substitution. * "in" and "out" filepath are relative to the current work directory for every single step (not relative to the path of the inputfile) * "in" and "out" can be the same * "out_mode" is optional, can be "w" or "a" (default: "w") * "w" : "out"-file will be overridden * "a" : "out"-file will be appended jube_pattern List of available jube pattern: * "$jube_pat_int": integer number * "$jube_pat_nint": integer number, skip * "$jube_pat_fp": floating point number * "$jube_pat_nfp": floating point number, skip * "$jube_pat_wrd": word * "$jube_pat_nwrd": word, skip * "$jube_pat_bl": blank space (variable length), skip jube_variables List of available jube variables: * Benchmark: * "$jube_benchmark_name": current benchmark name * "$jube_benchmark_id": current benchmark id * "$jube_benchmark_padid": current benchmark id with preceding zeros * "$jube_benchmark_home": original input file location * "$jube_benchmark_rundir": main benchmark specific execution directory * "$jube_benchmark_start": benchmark starting time * Step: * "$jube_step_name": current step name * "$jube_step_iterations": number of step iterations (default: 1) * "$jube_step_cycles": number of step cycles (default: 1) * Workpackage: * "$jube_wp_id": current workpackage id * "$jube_wp_padid": current workpackage id with preceding zeros * "$jube_wp_status": current workpackage status [RUNNING, DONE, ERROR] * "$jube_wp_iteration": current iteration number (default: 0) * "$jube_wp_parent__id": workpackage id of selected parent step * "$jube_wp_relpath": relative path to workpackage work directory (relative towards configuration file) * "$jube_wp_abspath": absolute path to workpackage work directory * "$jube_wp_envstr": a string containing all exported parameter in shell syntax: export par=$par export par2=$par2 * "$jube_wp_envlist": list of all exported parameter names * "$jube_wp_cycle": id of current step cycle (starts at 0) key_tag A syslog result key. "" must contain an single parameter- or patternname. ... * "title" is optional: alternative key title * "format" can contain a C like format string: e.g. "format=".2f"" link_tag A link can be used to create a symbolic link from your sandbox work directory to a file or directory inside your normal filesystem. ... * "source_dir" is optional, will be used as a prefix for the source filenames * "target_dir" is optional, will be used as a prefix for the target filenames * "name" is optional, it can be used to rename the file inside your work directory (will be ignored if you use shell extensions in your pathname) * "rel_path_ref" is optional * "external" or "internal" can be chosen, default: external * "external": rel.-paths based on position of xml-file * "internal": rel.-paths based on current work directory (e.g. to link files of another step) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * each link-tag can contain a list of filenames (or directories), separated by ",", the default separator can be changed by using the "separator" attribute * if "name" is present, the lists must have the same length * in the execution step the given files or directories will be linked log Show logs for the given benchmark directory or a given benchmark. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. output Shows path and content of the stdout and stderr files of the given benchmark. If no benchmark id is given, last benchmark found in will be used. parameter_space The parameter space for a specific benchmark run is the bundle of all possible parameter combinations. E.g. there are to different parameter: a = 1,2 and b= "p","q" then you will get four different parameter combinations: "a=1", "b="p""; "a=1", "b="q""; "a=2", "b="p""; "a=2", "b="q"". The parameter space of a specific step will be one of these parameter combinations. To fulfill all combinations the step will be executed multible times (each time using a new combination). The specific combination of a step and an expanded parameter space is named *workpackage*. parameter_tag A parameter can be used to store benchmark configuration data. A set of different parameters will create a specific parameter environment (also called *parameter space*) for the different steps of the benchmark. ... * a parameter can be seen as variable: Name is the name to use the variable, and the text between the tags will be the real content * name must be unique inside the given parameterset * "type" is optional (only used for sorting, default: "string") * "mode" is optional (used for script-types, default: "text") * "separator" is optional, default: "," * "export" is optional, if set to "true" the parameter will be exported to the shell environment when using "" * "unit" is optional, will be used in the result table * if the text contains the given (or the implicit) separator, a template will be created * use of another parameter: * inside the parameter definition, a parameter can be reused: "... $nameofparameter ..." * the parameter will be replaced multiple times (to handle complex parameter structures; max: 5 times) * the substitution will be run before the execution step starts with the current *parameter space*. Only parameters reachable in this step will be usable for substitution! * Scripting modes allowed: * "mode="python"": allow *Python* snippets (using "eval ") * "mode="perl"": allow *Perl* snippets (using "perl -e "print "") * "mode="shell"": allow *Shell* snippets * "mode="env"": include the content of an available environment variable * "mode="tag"": include the tag name if the tag was set during execution, otherwise the content is empty * Templates can be created, using scripting e.g.: "",".join([str(2**i) for i in range(3)])" * "update_mode" is optional (default: "never") * can be set to "never", "use", "step", "cycle" and "always" * depending on the setting the parameter will be reevaluated: * "never": no reevaluation, even if the parameterset is used multiple times * "use": reevaluation if the parameterset is explicitly used * "step": reevaluation in each new step * "cycle": reevaluation in each cycle, but not at the begin of a new step (number of workpackages will stay unchanged) * "always": reevaluation in each step and cycle * "duplicate" is optional and of relevance, if there are more than one parameter definitions with the same name within one parameterset. This "duplicate" option has higher priority than the "duplicte" option of the parameterset. "duplicate" must contain one of the following four options: * "none" (default): The "duplicate" option of the parameterset is prioritized * "replace": Parameters with the same name are overwritten * "concat": Parameters with the same name are concatenated * "error": Throws an error, if parameters with the same name are defined parameterset_tag A parameterset is a container to store a bundle of *parameters*. ... ... * parameterset-name must be unique (cannot be reused inside substitutionsets or filesets) * "init_with" is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a parameterset using the given name, all parameters will be copied to the local set * local parameters will overwrite imported parameters * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * parametersets can be used inside the step-command * parametersets can be combined inside the step-tag, but they must be compatible: * Two parametersets are compatible if the parameter intersection (given by the parameter-name), only contains parameter based on the same definition * These two sets are compatible: 1,2,4 foo 1,2,4 bar * These two sets are not compatible: 1,2,4 foo 2 bar * "duplicate" is optional and of relevance, if there are more than one parameter definitions with the same name within one parameterset. This "duplicate" option has lower priority than the "duplicte" option of the parameters. "duplicate" must contain one of the following three options: * "replace" (default): Parameters with the same name are overwritten * "concat": Parameters with the same name are concatenated * "error": Throws an error, if parameters with the same name are defined pattern_tag A pattern is used to parse your output files and create your result data. ... * "unit" is optional, will be used in the result table * "mode" is optional, allowed modes: * "pattern": a regular expression (default) * "text": simple text and variable concatenation * "perl": snippet evaluation (using *Perl*) * "python": snippet evaluation (using *Python*) * "shell": snippet evaluation (using *Shell*) * "type" is optional, specify datatype (for sort operation) * default: "string" * allowed: "int", "float" or "string" * "default" is optional: Specify default value if pattern cannot be found or if it cannot be evaluated * "dotall" is optional (default: "false"): Can be set to "true" or "false" to specify if a "." within the regular expression should also match newline characters, which can be very helpfull to extract a line only after a specific header was mentioned. patternset_tag A patternset is a container to store a bundle of patterns. ... ... * patternset-name must be unique * "init_with" is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a patternset using the given name, all pattern will be copied to the local set * local pattern will overwrite imported pattern * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * patternsets can be used inside the analyser tag * different sets, which are used inside the same analyser, must be compatible prepare_tag The prepare can contain any *Shell* command you want. It will be executed like a normal ** inside the step where the corresponding fileset is used. The only difference towards the normal do is, that it will be executed **before** the substitution will be executed. ... * "stdout"- and "stderr"-filename are optional (default: "stdout" and "stderr") * "work_dir" is optional, it can be used to change the work directory of this single command (relativly seen towards the original work directory) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute remove The given benchmark will be removed. If no benchmark id is given, last benchmark found in directory will be removed. Only the *JUBE* internal directory structure will be deleted. External files and directories will stay unchanged. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. result Create a result table. If no benchmark id is given, last benchmark found in directory will be used. If multiple benchmarks are selected (e.g. by using "--id all"), a combined result view of all available benchmarks in the given directory will be created. If benchmark directory is missing, current directory will be used. result_tag The result tag is used to handle different visualisation types of your analysed data. ... ... ...
... ... ...
* "result_dir" is optional. Here you can specify an different output directory. Inside of this directory a subfolder named by the current benchmark id will be created. Default: benchmark_dir/result * only analyser are usable * using analyser "set1,set2" is the same as "set1set2" run Start a new benchmark run by parsing the given *JUBE* input file. selection_tag Specify tags or select benchmarks by name. ... ... ... ... * multiple are allowed to specify tags (see tagging) * select or unselect a benchmark by name * only selected benchmarks will run (when using the "run" command) * multiple "" and "" are allowed * "" and "" can contain a name list divided by "," statistical_values If there are multiple pattern matches within one file, multiple files or when using multiple iterations. *JUBE* will create some statistical values automatically: * "first": first match (default) * "last": last match * "min": min value * "max": max value * "avg": average value * "std": standard deviation * "sum": sum * "cnt": counter These variabels can be accessed within the the result creation or to create derived pattern by "variable_name_" e.g. "${nodes_min}" The variable name itself always matches the first match. status Show status string (RUNNING or FINISHED) for the given benchmark. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. step_tag A step give a list of *Shell* operations and a corresponding parameter environment. ... ... ... * parametersets, filesets and substitutionsets are usable * using sets "set1,set2" is the same as "set1set2" * parameter can be used inside the ""-tag * the "from" attribute is optional and can be used to specify an external set source * any name must be unique, it is **not allowed to reuse** a set * "depend" is optional and can contain a list of other step names which must be executed before the current step * "max_async" is optional and can contain a number (or a parameter) which describe how many *workpackages* can be executed asynchronously (default: 0 means no limitation). This option is only important if a *do* inside the step contains a "done_file" attribute and should be executed in the background (or managed by a jobsystem). In this case *JUBE* will manage that there will not be to many instances at the same time. To update the benchmark and start further instances, if the first ones were finished, the *continue* command must be used. * "work_dir" is optional and can be used to switch to an alternative work directory * the user had to handle **uniqueness of this directory** by his own * no automatic parent/children link creation * "suffix" is optional and can contain a string (parameters are allowed) which will be attached to the default workpackage directory name * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * "shared" is optional and can be used to create a shared folder which can be accessed by all workpackages based on this step * a link, named by the attribute content, is used to access the shared folder * the shared folder link will not be automatically created in an alternative working directory! * "export="true"" * the environment of the current step will be exported to an dependent step * "iterations" is optional. All workpackages within this step will be executed multiple times if the iterations value is used. * "cycles" is optional. All "" commands within the step will be executed "cycles"-times * "procs" is optional. Amount of processes used to execute the parameter expansions of the corresponding step in parallel. * "do_log_file" is optional. Name or path of a do log file trying to mimick the do steps and the environment of a workpacakge of a step to produce an executable script. sub_tag A substition expression. * "source"-string will be replaced by "dest"-string * both can contain parameter: "... $nameofparameter ..." * "mode" is optional (default: "text"). Can be used to switch between "text" and "regex" substitution substituteset_tag A substituteset is a container to store a bundle of *sub* commands. ... ... * "init_with" is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a substituteset using the given name, all iofile and sub will be copied to the local set * local "iofile" will overwrite imported ones based on "out", local "sub" will overwrite imported ones based on "source" * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * substitutesets can be used inside the step-command syslog_tag A syslog result type ... ... * Syslog deamon can be given by a "host" and "port" combination (default "port": 541) or by a socket "address" e.g.: "/dev/log" (mixing of host and address is not allowed) * "format" is optional: can contain a log format written in a pythonic way (default: "jube[%(process)s]: %(message)s") * "sort" is optional: can contain a list of parameter- or patternnames (separated by ,). Given patterntype or parametertype will be used for sorting * "" must contain an single parameter- or patternname * Unlike the result table, the unit attribute of a parameter or pattern is not taken into account. * "filter" is optional, it can contain a bool expression to show only specific result entries table_tag A simple ASCII based table ouput. ... ...
* "style" is optional; allowed styles: "csv", "pretty", "aligned"; default: "csv" * "separator" is optional; only used in csv-style, default: "," * "sort" is optional: can contain a list of parameter- or patternnames (separated by ,). Given patterntype or parametertype will be used for sorting * "" must contain an single parameter- or patternname * "transpose" is optional (default: "false") * "filter" is optional, it can contain a bool expression to show only specific result entries tagging Tagging is a simple way to mark parts of your input file to be includable or excludable. * Every available "" (not the root ""-tag) can contain a tag-attribute * The tag-attribute can contain a list of names: "tag="a,b,c"" or "not" names: "tag="a,!b,c"" * When running *JUBE*, multiple tags can be send to the input-file parser: jube run --tag a b * "" which does not contain one of these names will be hidden inside the include file * "" which does not contain any tag-attribute will stay inside the include file * "not" tags are more important than normal tags: "tag="a,!b,c"" and running with "a b" will hide the "" because the "!b" is more important than the "a" types *Parameter* and *Pattern* allow a type specification. This type is either used for sorting within the result table and is also used to validate the parameter content. The types are not used to convert parameter values, e.g. a floating value will stay unchanged when used in any other context even if the type int was specified. allowed types are: * "string" (this is also the default type) * "int" * "float" update Check if a newer JUBE version is available. update_mode The update mode is parameter attribute which can be used to control the reevaluation of the parameter content. These update modes are available: * "never": no reevaluation, even if the parameterset is used multiple times * "use": reevaluation if the parameterset is explicitly used * "step": reevaluation in each new step * "cycle": reevaluation in each cycle, but not at the begin of a new step (number of workpackages will stay unchanged) * "always": reevaluation in each step and cycle workpackage A workpackage is the combination of a *step* (which contains all operations) and one parameter setting out of the expanded *parameter space*. Every workpackage will run inside its own sandbox directory! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/info.py0000664000174700017470000003521114603772010016746 0ustar00gitlab-runnergitlab-runner## JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Gives benchmark related info""" from __future__ import (print_function, unicode_literals, division) import jube2.util.util import jube2.util.output import jube2.conf import jube2.jubeio import os import time import textwrap import operator def print_benchmarks_info(path): """Print list of all benchmarks, found in given directory""" # Get list of all files and directories in given path if not os.path.isdir(path): raise OSError("Not a directory: \"{0}\"".format(path)) dir_list = os.listdir(path) benchmark_info = list() # Search for possible benchmark dirs for dir_name in dir_list: dir_path = os.path.join(path, dir_name) configuration_file = \ os.path.join(dir_path, jube2.conf.CONFIGURATION_FILENAME) if os.path.isdir(dir_path) and os.path.exists(configuration_file): try: id_number = int(dir_name) parser = jube2.jubeio.Parser(configuration_file) name_str, comment_str, tags = parser.benchmark_info_from_xml() tags_str = jube2.conf.DEFAULT_SEPARATOR.join(tags) # Read timestamps from timestamps file timestamps = \ jube2.util.util.read_timestamps( os.path.join(dir_path, jube2.conf.TIMESTAMPS_INFO)) if "start" in timestamps: time_start = timestamps["start"] else: time_start = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(configuration_file))) if "change" in timestamps: time_change = timestamps["change"] else: time_change = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getmtime(dir_path))) benchmark_info.append([id_number, name_str, time_start, time_change, comment_str, tags_str]) except ValueError: pass # sort using id benchmark_info = sorted(benchmark_info, key=operator.itemgetter(0)) # convert id to string for info in benchmark_info: info[0] = str(info[0]) # add header benchmark_info = [("id", "name", "started", "last change", "comment", "tags")] + benchmark_info if len(benchmark_info) > 1: infostr = (jube2.util.output.text_boxed("Benchmarks found in \"{0}\":". format(path)) + "\n" + jube2.util.output.text_table(benchmark_info, use_header_line=True)) print(infostr) else: print("No Benchmarks found in \"{0}\"".format(path)) def print_benchmark_info(benchmark): """Print information concerning a single benchmark""" infostr = \ jube2.util.output.text_boxed("{0} id:{1} tags:{2}\n\n{3}" .format(benchmark.name, benchmark.id, jube2.conf.DEFAULT_SEPARATOR.join( benchmark.tags), benchmark.comment)) print(infostr) continue_possible = False print(" Directory: {0}" .format(os.path.abspath(benchmark.bench_dir))) # Read timestamps from timestamps file timestamps = jube2.util.util.read_timestamps( os.path.join(benchmark.bench_dir, jube2.conf.TIMESTAMPS_INFO)) if "start" in timestamps: time_start = timestamps["start"] else: # Starttime is workpackage.xml creation time time_start = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(os.path.join( benchmark.bench_dir, jube2.conf.CONFIGURATION_FILENAME)))) if "change" in timestamps: time_change = timestamps["change"] else: time_change = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getmtime(benchmark.bench_dir))) print("\n Started: {0}".format(time_start)) print("Last change: {0}".format(time_change)) # Create step overview step_info = [("step name", "depends", "#work", "#error", "#done", "last finished")] for step_name, workpackages in benchmark.workpackages.items(): cnt_done = 0 cnt_error = 0 last_finish = time.localtime(0) depends = jube2.conf.DEFAULT_SEPARATOR.join( benchmark.steps[step_name].depend) for workpackage in workpackages: if workpackage.done: cnt_done += 1 # Read timestamp from done_file if it is available otherwise # use mtime done_file = os.path.join(workpackage.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) done_file_f = open(done_file, "r") done_str = done_file_f.read().strip() done_file_f.close() try: done_time = time.strptime(done_str, "%Y-%m-%d %H:%M:%S") except ValueError: done_time = time.localtime(os.path.getmtime(done_file)) last_finish = max(last_finish, done_time) if workpackage.error: cnt_error += 1 if last_finish > time.localtime(0): last_finish_str = time.strftime("%Y-%m-%d %H:%M:%S", last_finish) else: last_finish_str = "" continue_possible = continue_possible or \ (len(workpackages) != cnt_done) # Create #workpackages string iterations = benchmark.steps[step_name].iterations if benchmark.steps[step_name].iterations > 1: cnt = "{0}*{1}".format(len(workpackages) // iterations, iterations) else: cnt = str(len(workpackages)) step_info.append((step_name, depends, cnt, str(cnt_error), str(cnt_done), last_finish_str)) print( "\n" + jube2.util.output.text_table(step_info, use_header_line=True, indent=1)) if continue_possible: print("\n--- Benchmark not finished! ---\n") else: print("\n--- Benchmark finished ---\n") print(jube2.util.output.text_line()) def print_step_info(benchmark, step_name, parametrization_only=False, parametrization_only_csv=False): """Print information concerning a single step in a specific benchmark""" if step_name not in benchmark.workpackages: print("Step \"{0}\" not found in benchmark \"{1}\"." .format(step_name, benchmark.name)) return if parametrization_only_csv: parametrization_only = True if not parametrization_only: print(jube2.util.output.text_boxed( "{0} Step: {1}".format(benchmark.name, step_name))) step = benchmark.steps[step_name] # Get all possible error filenames error_file_names = set() for operation in step.operations: if operation.stderr_filename is not None: error_file_names.add(operation.stderr_filename) else: error_file_names.add("stderr") wp_info = [("id", "started?", "error?", "done?", "work_dir")] error_dict = dict() parameter_list = list() useable_parameter = None for workpackage in benchmark.workpackages[step_name]: # Parameter substitution to use alt_work_dir parameter = \ dict([[par.name, par.value] for par in workpackage.parameterset.constant_parameter_dict.values()]) # Save available parameter names if useable_parameter is None: useable_parameter = [name for name in parameter.keys()] useable_parameter.sort() id_str = str(workpackage.id) started_str = str(workpackage.started).lower() error_str = str(workpackage.error).lower() done_str = str(workpackage.done).lower() work_dir = workpackage.work_dir if step.alt_work_dir is not None: work_dir = jube2.util.util.substitution(step.alt_work_dir, parameter) # collect parameterization parameter_list.append(dict()) parameter_list[-1]["id"] = str(workpackage.id) for parameter in workpackage.parameterset: parameter_list[-1][parameter.name] = parameter.value # Read error-files for error_file_name in error_file_names: if os.path.exists(os.path.join(work_dir, error_file_name)): error_file = open(os.path.join(work_dir, error_file_name), "r") error_string = error_file.read().strip() if len(error_string) > 0: error_dict[os.path.abspath(os.path.join( work_dir, error_file_name))] = error_string error_file.close() # Store info data wp_info.append( (id_str, started_str, error_str, done_str, os.path.abspath(work_dir))) if not parametrization_only: print("Workpackages:") print(jube2.util.output.text_table(wp_info, use_header_line=True, indent=1, auto_linebreak=False)) if (useable_parameter is not None) and (not parametrization_only): print("Available parameter:") wraps = textwrap.wrap(", ".join(useable_parameter), 80) for wrap in wraps: print(wrap) print("") if not parametrization_only: print("Parameterization:") for parameter_dict in parameter_list: print(" ID: {0}".format(parameter_dict["id"])) for name, value in parameter_dict.items(): if name != "id": print(" {0}: {1}".format(name, value)) print("") else: # Create parameterization table table_data = list() table_data.append(list()) table_data[0].append("id") if len(parameter_list) > 0: for name in parameter_list[0]: if name != "id": table_data[0].append(name) for parameter_dict in parameter_list: table_data.append(list()) for name in table_data[0]: table_data[-1].append(parameter_dict[name]) print(jube2.util.output.text_table( table_data, use_header_line=True, indent=1, align_right=True, auto_linebreak=False, style="csv" if parametrization_only_csv else "pretty", separator=(parametrization_only_csv if (parametrization_only_csv) else None))) if not parametrization_only: if len(error_dict) > 0: print("!!! Errors found !!!:") for error_file in error_dict: print(">>> {0}:".format(error_file)) try: print("{0}\n".format(error_dict[error_file])) except UnicodeDecodeError: print("\n") def print_workpackage_info(benchmark, workpackage): """Print information concerning a single workpackage in a specific benchmark""" print(jube2.util.output.text_boxed( "{0} Workpackage with ID {1}".format(benchmark.name, workpackage.id))) print("Step: {}".format(workpackage.step.name)) print("") # Get and print workpackage status if workpackage.error: status = "ERROR" elif not workpackage.done: status = "RUNNING" else: status = "DONE" print("Status: {}".format(status)) print("") print("Iteration {}".format(workpackage.iteration)) print("Cycle {}".format(workpackage.cycle)) print("") # Print parents id if workpackage.parents: parent_str = "Workpackage Parents by ID: " for parent in workpackage.parents: parent_str += "{}".format(parent.id) print(parent_str) # Print sibling id if workpackage.iteration_siblings: sibling_str = "Iteration Sibling by ID: " for sibling in workpackage.iteration_siblings: sibling_str += "{}".format(sibling.id) print(sibling_str) print("") # Print parameterization print("Parameterization:") for parameter in workpackage.parameterset: if parameter.name != "id": print(" {0}: {1}".format(parameter.name, parameter.value)) print("") # Print environments if workpackage.env: env_str = "" for env_name, value in workpackage.env.items(): if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in os.environ or os.environ[env_name] != value): env_str += " {0}: {1}\n".format(env_name, value) if env_str: print("Environment:") print(env_str) print("") if os.environ: env_str = "" for env_name in os.environ: if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in workpackage.env): env_str += " {0}\n".format(env_name) if env_str: print("Environment:") print(env_str) print("") def print_benchmark_status(benchmark): """Print overall workpackage status in the following order RUNNING: At least one WP is still active ERROR: At least one WP raised an errror FINISHED: All WPs are finalized and no error was raised """ error = False running = False for step_name in benchmark.workpackages: for workpackage in benchmark.workpackages[step_name]: running = \ (not workpackage.done and not workpackage.error) or running error = workpackage.error or error if running: print("RUNNING") elif error: print("ERROR") else: print("FINISHED") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/jubeio.py0000664000174700017470000022575114603772010017302 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Basic I/O module""" from __future__ import (print_function, unicode_literals, division) import xml.etree.ElementTree as ET import os from jube2.util.util import Queue import jube2.benchmark import jube2.substitute import jube2.parameter import jube2.fileset import jube2.pattern import jube2.workpackage import jube2.analyser import jube2.step import jube2.util.util import jube2.util.output import jube2.conf import jube2.result_types.syslog import jube2.result_types.table import jube2.result_types.database import jube2.util.yaml_converter import sys import re import copy import hashlib import jube2.log from jube2.util.version import StrictVersion LOGGER = jube2.log.get_logger(__name__) class Parser(object): """JUBE XML input file parser""" def __init__(self, filename, tags=None, include_path=None, force=False, strict=False): self._filename = filename if include_path is None: include_path = list() self._include_path = include_path if tags is None: tags = set() self._tags = tags self._force = force self._strict = strict self._file_handle = None def __del__(self): if self._file_handle is not None: self._file_handle.close() @property def file_path_ref(self): """Return file path given by config file""" file_path_ref = os.path.dirname(self._filename) if len(file_path_ref) > 0: return file_path_ref else: return "." def benchmarks_from_xml(self): """Return a dict of benchmarks Here parametersets are global and accessible to all benchmarks defined in the corresponding XML file. """ benchmarks = dict() LOGGER.debug("Parsing {0}".format(self._filename)) if not os.path.isfile(self._filename): raise IOError("Benchmark configuration file not found: \"{0}\"" .format(self._filename)) tree = self._tree_from_file(self._filename) # Check compatible terminal encoding: In some cases, the terminal env. # only allow ascii based encoding, print and filesystem operation will # be broken if there is a special char inside the input file. # In such cases the encode will stop, using an UnicodeEncodeError try: xml = jube2.util.output.element_tree_tostring(tree.getroot(), encoding="UTF-8") xml.encode(sys.getfilesystemencoding()) except UnicodeEncodeError as uee: raise ValueError("Your terminal only allows '{0}' encoding. {1}" .format(sys.getfilesystemencoding(), str(uee))) # Check input file version version = tree.getroot().get("version") if (version is not None) and (not self._force): version = version.strip() if StrictVersion(version) > StrictVersion(jube2.conf.JUBE_VERSION): if self._strict: error_str = ("Benchmark file \"{0}\" was created using " + "a newer version of JUBE ({1}).\nCurrent " + "JUBE version ({2}) might not be compatible" + ". Due to strict mode, further execution " + "was stopped.").format( self._filename, version, jube2.conf.JUBE_VERSION) raise ValueError(error_str) else: info_str = ("Benchmark file \"{0}\" was created using a " + "newer version of JUBE ({1}).\nCurrent JUBE " + "version ({2}) might not be compatible." + "\nContinue? (y/n):").format( self._filename, version, jube2.conf.JUBE_VERSION) try: inp = raw_input(info_str) except NameError: inp = input(info_str) if not inp.startswith("y"): return None, list(), list() valid_tags = ["selection", "include-path", "parameterset", "benchmark", "substituteset", "fileset", "include", "patternset", "check_tags"] # Save init include path (from command line) init_include_path = list(self._include_path) # Preprocess xml-tree, this must be done multiple times because of # recursive include structures changed = True counter = 0 while changed and counter < jube2.conf.PREPROCESS_MAX_ITERATION: # Reset variables only_bench = set() not_bench = set() local_tree = copy.deepcopy(tree) self._include_path = list(init_include_path) counter += 1 LOGGER.debug(" --> Preprocess run {0} <--".format(counter)) LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join( self._tags))) Parser._remove_invalid_tags(local_tree.getroot(), self._tags) # Read selection area for selection_tree in local_tree.findall("selection"): new_only_bench, new_not_bench, new_tags = \ Parser._extract_selection(selection_tree) self._tags.update(new_tags) only_bench.update(new_only_bench) not_bench.update(new_not_bench) LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join( self._tags))) # Reset tree, because selection might add additional tags local_tree = copy.deepcopy(tree) Parser._remove_invalid_tags(local_tree.getroot(), self._tags) # Read include-path for include_path_tree in local_tree.findall("include-path"): self._extract_include_path(include_path_tree) # Add env var based include path self._include_path += Parser._read_envvar_include_path() # Add local dir to include path self._include_path += [self.file_path_ref] # Preprocess xml-tree LOGGER.debug(" Preprocess xml tree") for path in self._include_path: LOGGER.debug(" path: {0}".format(path)) changed = self._preprocessor(tree.getroot()) if changed: LOGGER.debug(" New tags might be included, start " + "additional include-preprocess run.") else: LOGGER.debug(" No preprocessing changes were detected, stop" + " additional include-preprocess runs.") # Rerun removing invalid tags LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join(self._tags))) Parser._remove_invalid_tags(tree.getroot(), self._tags) # Check tags for element in tree.getroot(): Parser._check_tag(element, valid_tags) # Check for remaing tags node = jube2.util.util.get_tree_element(tree.getroot(), tag_path="include") if node is not None: raise ValueError(("Remaining include element found, which " + "was not replaced (e.g. due to a missing " + "include-path):\n" + "") .format(node.attrib["from"])) # Read all global check_tags and check if necessary tags are given check_tags = "" for element in tree.findall("check_tags"): check_tags += "(" + element.text + ") + " if check_tags != "": check_tags = check_tags[:-3] # Remove last + if not jube2.util.util.valid_tags(check_tags, self._tags): raise ValueError("The following tag combination is required: " "{0}".format(check_tags.replace('|', ' or ')\ .replace('+', ' and ').replace('!', ' not '))) LOGGER.debug(" Preprocess done") # Read all global parametersets global_parametersets = self._extract_parametersets(tree) # Read all global substitutesets global_substitutesets = self._extract_substitutesets(tree) # Read all global filesets global_filesets = self._extract_filesets(tree) # Read all global patternsets global_patternsets = self._extract_patternsets(tree) # At this stage we iterate over benchmarks benchmark_list = tree.findall("benchmark") for benchmark_tree in benchmark_list: self._benchmark_preprocessor(benchmark_tree) benchmark = self._create_benchmark(benchmark_tree, global_parametersets, global_substitutesets, global_filesets, global_patternsets) benchmarks[benchmark.name] = benchmark return benchmarks, list(only_bench), list(not_bench) @staticmethod def _convert_old_tag_format(input_string): """Converts the old ,-based tag format into the new tag format""" tags = set(map(lambda x: x.strip(), input_string.split(","))) not_tags = set([tag for tag in tags if tag[0] == "!"]) tags = tags.difference(not_tags) output_string = "+".join(not_tags) if len(output_string) > 0 and len(tags) > 0: output_string += "+" if len(tags) > 0: output_string += "(" + "|".join(tags) + ")" return output_string @staticmethod def _check_valid_tags(element, tags): """Check if element contains only valid tags""" return jube2.util.util.valid_tags(element.get("tag"), tags) @staticmethod def _remove_invalid_tags(etree, tags): """Remove tags which contain an invalid tags-attribute""" children = list(etree) for child in children: if not Parser._check_valid_tags(child, tags): etree.remove(child) continue Parser._remove_invalid_tags(child, tags) def _preprocessor(self, etree): """Preprocess the xml-file by replacing include-tags""" children = list(etree) new_children = list() include_index = 0 changed = False for child in children: # Replace include tags if ((child.tag == "include") and Parser._check_valid_tags(child, self._tags)): filename = Parser._attribute_from_element(child, "from") path = child.get("path", ".") if path == "": path = "." try: file_path = self._find_include_file(filename) include_tree = ET.parse(file_path) # Find external nodes includes = include_tree.findall(path) except ValueError: includes = list() except ET.ParseError: LOGGER.error("Error while parsing {0}:".format(file_path)) raise if len(includes) > 0: # Remove include-node etree.remove(child) # Insert external nodes for include in includes: etree.insert(include_index, include) include_index += 1 new_children.append(include) include_index -= 1 changed = True else: new_children.append(child) include_index += 1 for child in new_children: changed = self._preprocessor(child) or changed return changed def _benchmark_preprocessor(self, benchmark_etree): """Preprocess the xml-tree of given benchmark.""" LOGGER.debug(" Preprocess benchmark xml tree") # Search for and load external set uses = jube2.util.util.get_tree_elements(benchmark_etree, "use") files = dict() for use in uses: from_str = use.get("from", "").strip() if (use.text is not None) and (use.text.strip() != "") and \ (from_str != ""): hash_val = hashlib.md5(from_str.encode()).hexdigest() if hash_val not in files: files[hash_val] = set() set_names = [element.strip() for element in use.text.split(jube2.conf.DEFAULT_SEPARATOR)] for file_str in from_str.split(jube2.conf.DEFAULT_SEPARATOR): parts = file_str.strip().split(":") filename = parts[0].strip() if filename == "": filename = self._filename alt_set_names = set([element.strip() for element in parts[1:]]) if len(alt_set_names) == 0: alt_set_names = set(set_names) for name in alt_set_names: files[hash_val].add((filename, name)) # Replace set-name with an internal one new_use_str = "" for name in set_names: if len(new_use_str) > 0: new_use_str += jube2.conf.DEFAULT_SEPARATOR new_use_str += "jube_{0}_{1}".format(hash_val, name) use.text = new_use_str # Create new xml elements for fileid in files: for filename, name in files[fileid]: set_type = self._find_set_type(filename, name) set_etree = ET.SubElement(benchmark_etree, set_type) set_etree.attrib["name"] = "jube_{0}_{1}".format(fileid, name) set_etree.attrib["init_with"] = "{0}:{1}".format( filename, name) LOGGER.debug(" Created new <{0}>: jube_{1}_{2}".format( set_type, fileid, name)) def _find_include_file(self, filename): """Search for filename in include-pathes and return resulting path""" for path in self._include_path: file_path = os.path.join(path, filename) if os.path.exists(file_path): break else: raise ValueError(("\"{0}\" not found in possible " + "include pathes").format(filename)) return file_path def _find_set_type(self, filename, name): """Search for the set-type inside given file""" LOGGER.debug( " Searching for type of \"{0}\" in {1}".format(name, filename)) file_path = self._find_include_file(filename) etree = self._tree_from_file(file_path).getroot() Parser._remove_invalid_tags(etree, self._tags) found_set = jube2.util.util.get_tree_elements( etree, attribute_dict={"name": name}) found_set = [set_etree for set_etree in found_set if set_etree.tag in ("parameterset", "substituteset", "fileset", "patternset")] if len(found_set) > 1: raise ValueError(("name=\"{0}\" can be found multiple times " + "inside \"{1}\"").format(name, file_path)) elif len(found_set) == 0: raise ValueError(("name=\"{0}\" not found inside " + "\"{1}\"").format(name, file_path)) else: return found_set[0].tag def benchmark_info_from_xml(self): """Return name, comment and available tags of first benchmark found in file""" tree = ET.parse(self._filename).getroot() tags = set() for tag_etree in jube2.util.util.get_tree_elements(tree, "selection/tag"): if tag_etree.text is not None: tags.update(set([tag.strip() for tag in tag_etree.text.split( jube2.conf.DEFAULT_SEPARATOR)])) benchmark_etree = jube2.util.util.get_tree_element(tree, "benchmark") if benchmark_etree is None: raise ValueError("benchmark-tag not found in \"{0}\"".format( self._filename)) name = Parser._attribute_from_element(benchmark_etree, "name").strip() comment_element = benchmark_etree.find("comment") if comment_element is not None: comment = comment_element.text if comment is None: comment = "" else: comment = "" comment = re.sub(r"\s+", " ", comment).strip() return name, comment, tags def analyse_result_from_xml(self): """Read existing analyse out of xml-file""" LOGGER.debug("Parsing {0}".format(self._filename)) try: tree = ET.parse(self._filename).getroot() except ET.ParseError as pe: LOGGER.error( "Parsing error while reading existing analysis: " + "{0}".format(pe)) return None analyse_result = dict() analyser = jube2.util.util.get_tree_elements(tree, "analyzer") analyser += jube2.util.util.get_tree_elements(tree, "analyser") for analyser_etree in analyser: analyser_name = Parser._attribute_from_element( analyser_etree, "name") analyse_result[analyser_name] = dict() for step_etree in analyser_etree: Parser._check_tag(step_etree, ["step"]) step_name = Parser._attribute_from_element( step_etree, "name") analyse_result[analyser_name][step_name] = dict() for workpackage_etree in step_etree: Parser._check_tag(workpackage_etree, ["workpackage"]) wp_id = int(Parser._attribute_from_element( workpackage_etree, "id")) analyse_result[analyser_name][step_name][wp_id] = dict() for pattern_etree in workpackage_etree: Parser._check_tag(pattern_etree, ["pattern"]) pattern_name = \ Parser._attribute_from_element( pattern_etree, "name") pattern_type = \ Parser._attribute_from_element( pattern_etree, "type") value = pattern_etree.text if value is not None: value = value.strip() else: value = "" value = jube2.util.util.convert_type(pattern_type, value) analyse_result[analyser_name][step_name][ wp_id][pattern_name] = value return analyse_result def workpackages_from_xml(self, benchmark): """Read existing workpackage data out of a xml-file""" workpackages = dict() # tmp: Dict workpackage_id => workpackage tmp = dict() # parents_tmp: Dict workpackage_id => list of parent_workpackage_ids parents_tmp = dict() iteration_siblings_tmp = dict() work_list = Queue() LOGGER.debug("Parsing {0}".format(self._filename)) if not os.path.isfile(self._filename): raise IOError("Workpackage configuration file not found: \"{0}\"" .format(self._filename)) tree = ET.parse(self._filename) max_id = -1 for element in tree.getroot(): Parser._check_tag(element, ["workpackage"]) # Read XML-data (workpackage_id, step_name, parameterset, parents, iteration_siblings, iteration, cycle, set_env, unset_env) = \ Parser._extract_workpackage_data(element) # Search for step step = benchmark.steps[step_name] parameter_names = [parameter.name for parameter in parameterset] tmp[workpackage_id] = \ jube2.workpackage.Workpackage(benchmark, step, parameter_names, parameterset, workpackage_id, iteration, cycle) max_id = max(max_id, workpackage_id) parents_tmp[workpackage_id] = parents iteration_siblings_tmp[workpackage_id] = iteration_siblings tmp[workpackage_id].env.update(set_env) for env_name in unset_env: if env_name in tmp[workpackage_id].env: del tmp[workpackage_id].env[env_name] if len(parents) == 0: work_list.put(tmp[workpackage_id]) # Set workpackage counter to current id number jube2.workpackage.Workpackage.id_counter = max_id + 1 # Rebuild graph structure for workpackage_id in parents_tmp: for parent_id in parents_tmp[workpackage_id]: tmp[workpackage_id].add_parent(tmp[parent_id]) tmp[parent_id].add_children(tmp[workpackage_id]) # Rebuild sibling structure for workpackage_id in iteration_siblings_tmp: for sibling_id in iteration_siblings_tmp[workpackage_id]: tmp[workpackage_id].iteration_siblings.add(tmp[sibling_id]) # Rebuild history done_list = list() while not work_list.empty(): workpackage = work_list.get_nowait() history = jube2.parameter.Parameterset() if workpackage.id in parents_tmp: for parent_id in parents_tmp[workpackage.id]: history.add_parameterset(tmp[parent_id].parameterset) done_list.append(workpackage) for child in workpackage.children: all_done = True for parent in child.parents: all_done = all_done and (parent in done_list) if all_done and (child not in done_list): work_list.put(child) history.add_parameterset(workpackage.parameterset) workpackage.parameterset.add_parameterset(history) # Add JUBE parameter for workpackage in tmp.values(): # JUBE benchmark parameter workpackage.parameterset.add_parameterset( benchmark.get_jube_parameterset()) # JUBE step parameter workpackage.parameterset.add_parameterset( workpackage.step.get_jube_parameterset()) # JUBE workpackage parameter workpackage.parameterset.add_parameterset( workpackage.get_jube_parameterset()) # Enable work_dir caching workpackage.allow_workpackage_dir_caching() jube_parameter = workpackage.parameterset.get_updatable_parameter( jube2.parameter.JUBE_MODE) jube_parameter.parameter_substitution( additional_parametersets=[workpackage.parameterset], final_sub=True) workpackage.parameterset.update_parameterset(jube_parameter) # Update step parameter update_parameter = workpackage.parameterset.get_updatable_parameter( jube2.parameter.STEP_MODE) if len(update_parameter) > 0: fixed_parameterset = workpackage.parameterset.copy() for parameter in update_parameter: fixed_parameterset.delete_parameter(parameter) change = True while change: change = False update_parameter.parameter_substitution( [fixed_parameterset]) if update_parameter.has_templates: update_parameter = list( update_parameter.expand_templates())[0] change = True update_parameter.parameter_substitution( [fixed_parameterset], final_sub=True) workpackage.parameterset.update_parameterset(update_parameter) # Store workpackage data work_stat = jube2.util.util.WorkStat() for step_name in benchmark.steps: workpackages[step_name] = list() # First put started wps inside the queue for mode in ("only_started", "all"): for workpackage in tmp.values(): if len(workpackage.parents) == 0: if (mode == "only_started" and workpackage.started) or \ (mode == "all" and (not workpackage.queued)): workpackage.queued = True work_stat.put(workpackage) if mode == "all": workpackages[workpackage.step.name].append(workpackage) return workpackages, work_stat @staticmethod def _extract_workpackage_data(workpackage_etree): """Extract workpackage information from etree Return workpackage id, name of step, local parameterset and list of parent ids """ valid_tags = ["step", "parameterset", "parents", "iteration_siblings", "environment"] for element in workpackage_etree: Parser._check_tag(element, valid_tags) workpackage_id = int(Parser._attribute_from_element( workpackage_etree, "id")) step_etree = workpackage_etree.find("step") iteration = int(step_etree.get("iteration", "0").strip()) cycle = int(step_etree.get("cycle", "0").strip()) step_name = step_etree.text.strip() parameterset_etree = workpackage_etree.find("parameterset") if parameterset_etree is not None: parameters = Parser._extract_parameters(parameterset_etree) else: parameters = list() parameterset = jube2.parameter.Parameterset() for parameter in parameters: parameterset.add_parameter(parameter) parents_etree = workpackage_etree.find("parents") if parents_etree is not None: parents = [int(parent) for parent in parents_etree.text.split(",")] else: parents = list() siblings_etree = workpackage_etree.find("iteration_siblings") if siblings_etree is not None: iteration_siblings = set([int(sibling) for sibling in siblings_etree.text.split(",")]) else: iteration_siblings = set([workpackage_id]) environment_etree = workpackage_etree.find("environment") set_env = dict() unset_env = list() if environment_etree is not None: for env_etree in environment_etree: env_name = Parser._attribute_from_element(env_etree, "name") if env_etree.tag == "env": if env_etree.text is not None: set_env[env_name] = env_etree.text.strip() # string repr must be evaluated if (set_env[env_name][0] == "'") or \ ((set_env[env_name][0] == "u") and (set_env[env_name][1] == "'")) and \ (set_env[env_name][-1] == "'"): set_env[env_name] = eval(set_env[env_name]) elif env_etree.tag == "nonenv": unset_env.append(env_name) return (workpackage_id, step_name, parameterset, parents, iteration_siblings, iteration, cycle, set_env, unset_env) @staticmethod def _extract_selection(selection_etree): """Extract selction information from etree Return names of benchmarks and tags (set([only,...]),set([not,...]), set([tag, ...])) """ LOGGER.debug(" Parsing ") valid_tags = ["only", "not", "tag"] only_bench = list() not_bench = list() tags = set() for element in selection_etree: Parser._check_tag(element, valid_tags) separator = jube2.conf.DEFAULT_SEPARATOR if element.text is not None: if element.tag == "only": only_bench += element.text.split(separator) elif element.tag == "not": not_bench += element.text.split(separator) elif element.tag == "tag": tags.update(set([tag.strip() for tag in element.text.split(separator)])) only_bench = set([bench.strip() for bench in only_bench]) not_bench = set([bench.strip() for bench in not_bench]) return only_bench, not_bench, tags def _extract_include_path(self, include_path_etree): """Extract include-path pathes from etree""" LOGGER.debug(" Parsing ") valid_tags = ["path"] pathes = [] if (include_path_etree.text) and len(include_path_etree.text.strip()) > 0: pathes.append(include_path_etree.text.strip()) for element in include_path_etree: # Skip include tags that have not yet been replaced to allow include if element.tag == "include": continue Parser._check_tag(element, valid_tags) path = element.text if path is None: raise ValueError("Empty \"\" found") path = path.strip() if len(path) == 0: raise ValueError("Empty \"\" found") pathes.append(path) for path in pathes: path = os.path.expandvars(os.path.expanduser(path)) path = os.path.join(self.file_path_ref, path) self._include_path += [path] LOGGER.debug(" New path: {0}".format(path)) @staticmethod def _read_envvar_include_path(): """Add environment var include-path""" LOGGER.debug(" Read $JUBE_INCLUDE_PATH") if "JUBE_INCLUDE_PATH" in os.environ: return [include_path for include_path in os.environ["JUBE_INCLUDE_PATH"].split(":") if include_path != ""] else: return [] def _create_benchmark(self, benchmark_etree, global_parametersets, global_substitutesets, global_filesets, global_patternsets): """Create benchmark from etree Return a benchmark """ name = \ Parser._attribute_from_element(benchmark_etree, "name").strip() valid_tags = ["parameterset", "substituteset", "fileset", "step", "comment", "patternset", "analyzer", "analyser", "result"] for element in benchmark_etree: Parser._check_tag(element, valid_tags) comment_element = benchmark_etree.find("comment") if comment_element is not None: comment = comment_element.text if comment is None: comment = "" else: comment = "" comment = re.sub(r"\s+", " ", comment).strip() outpath = Parser._attribute_from_element(benchmark_etree, "outpath").strip() outpath = os.path.expandvars(os.path.expanduser(outpath)) # Add position of user to outpath outpath = os.path.normpath(os.path.join(self.file_path_ref, outpath)) file_path_ref = benchmark_etree.get("file_path_ref") # Combine global and local sets parametersets = \ Parser._combine_global_and_local_sets( global_parametersets, self._extract_parametersets(benchmark_etree)) substitutesets = \ Parser._combine_global_and_local_sets( global_substitutesets, self._extract_substitutesets(benchmark_etree)) filesets = \ Parser._combine_global_and_local_sets( global_filesets, self._extract_filesets(benchmark_etree)) patternsets = \ Parser._combine_global_and_local_sets( global_patternsets, self._extract_patternsets(benchmark_etree)) # dict of local steps steps = self._extract_steps(benchmark_etree) # dict of local analysers analyser = self._extract_analysers(benchmark_etree) # dict of local results results, results_order = self._extract_results(benchmark_etree) # File path reference for relative file location if file_path_ref is not None: file_path_ref = file_path_ref.strip() file_path_ref = \ os.path.expandvars(os.path.expanduser(file_path_ref)) else: file_path_ref = "." # Add position of user to file_path_ref file_path_ref = \ os.path.normpath(os.path.join(self.file_path_ref, file_path_ref)) benchmark = jube2.benchmark.Benchmark(name, outpath, parametersets, substitutesets, filesets, patternsets, steps, analyser, results, results_order, comment, self._tags, file_path_ref) return benchmark @staticmethod def _combine_global_and_local_sets(global_sets, local_sets): """Combine global and local sets """ result_sets = dict(global_sets) if set(result_sets) & set(local_sets): raise ValueError("\"{0}\" not unique" .format(",".join([name for name in (set(result_sets) & set(local_sets))]))) result_sets.update(local_sets) return result_sets @staticmethod def _extract_steps(etree): """Extract all steps from benchmark Return a dict of steps, e.g. {"compile": Step(...), ...} """ steps = dict() for element in etree.findall("step"): step = Parser._extract_step(element) if step.name in steps: raise ValueError("\"{0}\" not unique".format(step.name)) steps[step.name] = step return steps @staticmethod def _extract_step(etree_step): """Extract a step from etree Return name, list of contents (dicts), depend (list of strings). """ valid_tags = ["use", "do"] name = Parser._attribute_from_element(etree_step, "name").strip() LOGGER.debug(" Parsing ".format(name)) tmp = etree_step.get("depend", "").strip() iterations = int(etree_step.get("iterations", "1").strip()) alt_work_dir = etree_step.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() export = etree_step.get("export", "false").strip().lower() == "true" max_wps = etree_step.get("max_async", "0").strip() active = etree_step.get("active", "true").strip() suffix = etree_step.get("suffix", "").strip() cycles = int(etree_step.get("cycles", "1").strip()) procs = int(etree_step.get("procs", "1").strip()) do_log_file = etree_step.get("do_log_file", "None").strip() do_log_file = None if do_log_file == "None" else do_log_file do_log_file = None if do_log_file == "False" else do_log_file do_log_file = None if do_log_file == "false" else do_log_file do_log_file = jube2.conf.DO_LOG_FILENAME if do_log_file == "True" else do_log_file do_log_file = jube2.conf.DO_LOG_FILENAME if do_log_file == "true" else do_log_file shared_name = etree_step.get("shared") if shared_name is not None: shared_name = shared_name.strip() if shared_name == "": raise ValueError("Empty \"shared\" attribute in " + " found.") depend = set(val.strip() for val in tmp.split(jube2.conf.DEFAULT_SEPARATOR) if val.strip()) step = jube2.step.Step(name, depend, iterations, alt_work_dir, shared_name, export, max_wps, active, suffix, cycles, procs, do_log_file) for element in etree_step: Parser._check_tag(element, valid_tags) if element.tag == "do": async_filename = element.get("done_file") if async_filename is not None: async_filename = async_filename.strip() error_filename = element.get("error_file") if error_filename is not None: error_filename = error_filename.strip() break_filename = element.get("break_file") if break_filename is not None: break_filename = break_filename.strip() stdout_filename = element.get("stdout") if stdout_filename is not None: stdout_filename = stdout_filename.strip() stderr_filename = element.get("stderr") if stderr_filename is not None: stderr_filename = stderr_filename.strip() active = element.get("active", "true").strip() shared_str = element.get("shared", "false").strip() alt_work_dir = element.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() if shared_str.lower() == "true": if shared_name is None: raise ValueError(" only allowed " "inside a which has a shared " "region") if procs != 1: raise ValueError(" not allowed " + "inside a parallel ") shared = True elif shared_str == "false": shared = False else: raise ValueError("shared=\"{0}\" not allowed. Must be " + "\"true\" or \"false\"".format( shared_str)) cmd = element.text if cmd is None: cmd = "" operation = jube2.step.Operation(cmd.strip(), async_filename, stdout_filename, stderr_filename, active, shared, alt_work_dir, break_filename, error_filename) step.add_operation(operation) elif element.tag == "use": step.add_uses(Parser._extract_use(element)) return step @staticmethod def _extract_analysers(etree): """Extract all analyser from etree""" analysers = dict() analyser_tags = etree.findall("analyzer") analyser_tags += etree.findall("analyser") for element in analyser_tags: analyser = Parser._extract_analyser(element) if analyser.name in analysers: raise ValueError("\"{0}\" not unique".format(analyser.name)) analysers[analyser.name] = analyser return analysers @staticmethod def _extract_analyser(etree_analyser): """Extract an analyser from etree""" valid_tags = ["use", "analyse"] name = Parser._attribute_from_element(etree_analyser, "name").strip() reduce_iteration = \ etree_analyser.get("reduce", "true").strip().lower() == "true" analyser = jube2.analyser.Analyser(name, reduce_iteration) LOGGER.debug(" Parsing ".format(name)) for element in etree_analyser: Parser._check_tag(element, valid_tags) if element.tag == "analyse": step_name = Parser._attribute_from_element(element, "step").strip() # If there are no files, just add a dummy element to the list if len(element) == 0: analyser.add_analyse(step_name, None) for file_etree in element: if (file_etree.text is None) or \ (file_etree.text.strip() == ""): raise ValueError("Empty found") else: use_text = file_etree.get("use") if use_text is not None: use_names = \ [use_name.strip() for use_name in use_text.split(jube2.conf.DEFAULT_SEPARATOR)] else: use_names = list() for filename in file_etree.text.split( jube2.conf.DEFAULT_SEPARATOR): file_obj = jube2.analyser.Analyser.AnalyseFile( filename.strip()) file_obj.add_uses(use_names) analyser.add_analyse(step_name, file_obj) elif element.tag == "use": analyser.add_uses(Parser._extract_use(element)) return analyser @staticmethod def _extract_results(etree): """Extract all results from etree""" results = dict() results_order = list() valid_tags = ["use", "table", "syslog", "database"] for result_etree in etree.findall("result"): result_dir = result_etree.get("result_dir") if result_dir is not None: result_dir = \ os.path.expandvars(os.path.expanduser(result_dir.strip())) sub_results = dict() uses = list() for element in result_etree: Parser._check_tag(element, valid_tags) if element.tag == "use": uses.append(Parser._extract_use(element)) elif element.tag == "table": result = Parser._extract_table(element) result.result_dir = result_dir elif element.tag == "syslog": result = Parser._extract_syslog(element) elif element.tag == "database": result = Parser._extract_database(element) result.result_dir = result_dir if element.tag in ["table", "syslog", "database"]: if result.name in sub_results: raise ValueError( ("Result name \"{0}\" is used " + "multiple times").format(result.name)) sub_results[result.name] = result if result.name not in results_order: results_order.append(result.name) for result in sub_results.values(): for use in uses: result.add_uses(use) if len(set(results.keys()).intersection( set(sub_results.keys()))) > 0: raise ValueError( ("Result name(s) \"{0}\" is/are used " + "multiple times").format( ",".join(set(results.keys()).intersection( set(sub_results.keys()))))) results.update(sub_results) return results, results_order @staticmethod def _extract_table(etree_table): """Extract a table from etree""" name = Parser._attribute_from_element(etree_table, "name").strip() separator = \ etree_table.get("separator", jube2.conf.DEFAULT_SEPARATOR) style = etree_table.get("style", "csv").strip() if style not in ["csv", "pretty", "aligned"]: raise ValueError("Not allowed style-type \"{0}\" " "in ".format(style, name)) sort_names = etree_table.get("sort", "").split( jube2.conf.DEFAULT_SEPARATOR) sort_names = [sort_name.strip() for sort_name in sort_names] sort_names = [ sort_name for sort_name in sort_names if len(sort_name) > 0] transpose = etree_table.get("transpose") if transpose is not None: transpose = transpose.strip().lower() == "true" else: transpose = False res_filter = etree_table.get("filter") if res_filter is not None: res_filter = res_filter.strip() table = jube2.result_types.table.Table(name, style, separator, sort_names, transpose, res_filter) for element in etree_table: Parser._check_tag(element, ["column"]) column_name = element.text if column_name is None: column_name = "" column_name = column_name.strip() if column_name == "": raise ValueError("Empty not allowed") colw = element.get("colw") if colw is not None: colw = int(colw) title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() table.add_column(column_name, colw, format_string, title) return table @staticmethod def _extract_database(etree_database): """Extract a database result infos from etree""" name = Parser._attribute_from_element(etree_database, "name").strip() res_filter = etree_database.get("filter") if res_filter is not None: res_filter = res_filter.strip() primekeys = etree_database.get("primekeys", "") primekeys = primekeys.replace('[', '').replace(']', '').replace( "'", '').split(jube2.conf.DEFAULT_SEPARATOR) primekeys = [primekey.strip() for primekey in primekeys] primekeys = [primekey for primekey in primekeys if len(primekey) > 0] db_file = etree_database.get("file") database = jube2.result_types.database.Database( name, res_filter, primekeys, db_file) for element in etree_database: Parser._check_tag(element, ["key"]) key_name = element.text if key_name is None: key_name = "" key_name = key_name.strip() if key_name == "": raise ValueError("Empty not allowed") title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() database.add_key(key_name, format_string, title) return database @staticmethod def _extract_syslog(etree_syslog): """Extract requires syslog information from etree.""" name = Parser._attribute_from_element(etree_syslog, "name").strip() # see if the host, port combination or address is given syslog_address = etree_syslog.get("address") if syslog_address is not None: syslog_address = \ os.path.expandvars(os.path.expanduser(syslog_address.strip())) syslog_host = etree_syslog.get("host") if syslog_host is not None: syslog_host = syslog_host.strip() syslog_port = etree_syslog.get("port") if syslog_port is not None: syslog_port = int(syslog_port.strip()) syslog_fmt_string = etree_syslog.get("format") if syslog_fmt_string is not None: syslog_fmt_string = syslog_fmt_string.strip() sort_names = etree_syslog.get("sort", "").split( jube2.conf.DEFAULT_SEPARATOR) sort_names = [sort_name.strip() for sort_name in sort_names] sort_names = [ sort_name for sort_name in sort_names if len(sort_name) > 0] res_filter = etree_syslog.get("filter") if res_filter is not None: res_filter = res_filter.strip() syslog_result = jube2.result_types.syslog.SysloggedResult( name, syslog_address, syslog_host, syslog_port, syslog_fmt_string, sort_names, res_filter) for element in etree_syslog: Parser._check_tag(element, ["key"]) key_name = element.text if key_name is None: key_name = "" key_name = key_name.strip() if key_name == "": raise ValueError("Empty not allowed") title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() syslog_result.add_key(key_name, format_string, title) return syslog_result @staticmethod def _extract_use(etree_use): """Extract a use from etree""" if etree_use.text is not None: use_names = [use_name.strip() for use_name in etree_use.text.split(jube2.conf.DEFAULT_SEPARATOR)] return use_names else: raise ValueError("Empty found") def _tree_from_file(self, file_path): """Extract a XML tree from a file (doing implicit YAML conversion)""" try: if file_path.endswith(".xml"): return ET.parse(file_path) elif file_path.endswith(".yml") or file_path.endswith(".yaml") or \ jube2.util.yaml_converter.\ YAML_Converter.is_parseable_yaml_file(file_path): include_path = list(self._include_path) include_path += Parser._read_envvar_include_path() file_handle = jube2.util.yaml_converter.YAML_Converter( file_path, include_path, self._tags) data = file_handle.read() tree = ET.ElementTree(ET.fromstring(data)) file_handle.close() return tree else: return ET.parse(file_path) except Exception: LOGGER.error("Error while parsing {0}:".format(file_path)) raise def _extract_extern_set(self, filename, set_type, name, search_name=None, duplicate=None): """Load a parameter-/file-/substitutionset from a given file""" if search_name is None: search_name = name LOGGER.debug(" Searching for <{0} name=\"{1}\"> in {2}" .format(set_type, search_name, filename)) file_path = self._find_include_file(filename) etree = self._tree_from_file(file_path).getroot() Parser._remove_invalid_tags(etree, self._tags) self._preprocessor(etree) result_set = None # Find element in XML-tree elements = jube2.util.util.get_tree_elements(etree, set_type, {"name": search_name}) # Element can also be the root element itself if etree.tag == set_type: element = jube2.util.util.get_tree_element( etree, attribute_dict={"name": search_name}) if element is not None: elements.append(element) test_duplicate=None if duplicate == "###initiated_with_without_duplicate_mentioning###": if elements[0].get("duplicate") != None: duplicate = elements[0].get("duplicate") else: duplicate = "replace" if duplicate != "###initiated_with_without_duplicate_mentioning###" and duplicate != None: if set_type == "parameterset": if elements[0].get("duplicate") == None: test_duplicate = duplicate else: test_duplicate = elements[0].get("duplicate") if duplicate != None: if test_duplicate != duplicate: raise ValueError("The {0} {1} is mentioned at least twice with different duplicate options.".format(set_type, name)) if duplicate == "###initiated_with_without_duplicate_mentioning###": raise Exception("Unknown error in extracting an extern set." + "This should not happen. Please contact the JUBE developers.") if elements is not None: if len(elements) > 1: raise ValueError("\"{0}\" found multiple times in \"{1}\"" .format(search_name, file_path)) elif len(elements) == 0: raise ValueError("\"{0}\" not found in \"{1}\"" .format(search_name, file_path)) init_with = elements[0].get("init_with") # recursive external file open if init_with is not None: parts = init_with.strip().split(":") new_filename = parts[0] if len(parts) > 1: new_search_name = parts[1] else: new_search_name = search_name if (new_filename == filename) and \ (new_search_name == search_name): raise ValueError(("Cannot init <{0} name=\"{1}\"> by " "itself inside \"{2}\"").format( set_type, search_name, file_path)) result_set = self._extract_extern_set(new_filename, set_type, name, new_search_name, duplicate) if set_type == "parameterset": if result_set is None: result_set = jube2.parameter.Parameterset(name, duplicate) for parameter in self._extract_parameters(elements[0]): result_set.add_parameter(parameter) elif set_type == "substituteset": files, subs = self._extract_subs(elements[0]) if result_set is None: result_set = \ jube2.substitute.Substituteset(name, files, subs) else: result_set.update_files(files) result_set.update_substitute(subs) elif set_type == "fileset": if result_set is None: result_set = jube2.fileset.Fileset(name) files = self._extract_files(elements[0]) for file_obj in files: if type(file_obj) is not jube2.fileset.Prepare: file_obj.file_path_ref = \ os.path.join(os.path.dirname(file_path), file_obj.file_path_ref) if not os.path.isabs(file_obj.file_path_ref): file_obj.file_path_ref = \ os.path.relpath(file_obj.file_path_ref, self.file_path_ref) result_set += files elif set_type == "patternset": if result_set is None: result_set = jube2.pattern.Patternset(name) for pattern in self._extract_pattern(elements[0]): result_set.add_pattern(pattern) return result_set else: raise ValueError("\"{0}\" not found in \"{1}\"" .format(name, file_path)) def _extract_parametersets(self, etree): """Return parametersets from etree""" parametersets = dict() for element in etree.findall("parameterset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) duplicate = element.get("duplicate", "replace").strip() if duplicate is None: duplicate="replace" if duplicate != "replace" and duplicate != "concat" and duplicate != "error": raise ValueError("Invalid \"duplicate\" attribute in " + "parameterset {0} found. Use \"replace\" (default)" + ", \"concat\" or \"error\".".format(name)) init_with = element.get("init_with") if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None if element.get("duplicate") == None: duplicate = "###initiated_with_without_duplicate_mentioning###" parameterset = self._extract_extern_set(parts[0], "parameterset", name, search_name, duplicate) else: parameterset = jube2.parameter.Parameterset(name, duplicate) for parameter in self._extract_parameters(element): parameterset.add_parameter(parameter) if parameterset.name in parametersets: raise ValueError( "\"{0}\" not unique".format(parameterset.name)) parametersets[parameterset.name] = parameterset return parametersets @staticmethod def _extract_parameters(etree_parameterset): """Extract parameters from parameterset Return a list of parameters. Parameters might also include lists""" parameters = list() for param in etree_parameterset: Parser._check_tag(param, ["parameter"]) name = Parser._attribute_from_element(param, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") if not re.match(r"^[^\d\W]\w*$", name, re.UNICODE): raise ValueError(("name=\"{0}\" in " + "contains a disallowed " + "character").format(name)) separator = param.get("separator", default=jube2.conf.DEFAULT_SEPARATOR) parameter_type = param.get("type", default="string").strip() parameter_mode = param.get("mode", default="text").strip() parameter_unit = param.get("unit", default="").strip() parameter_update_mode = param.get("update_mode", default="never").strip() if parameter_update_mode not in jube2.parameter.UPDATE_MODES: raise ValueError( ("update_mode=\"{0}\" in " + " does not exist") .format(parameter_update_mode, name)) export_str = param.get("export", default="false").strip() export = export_str.lower() == "true" duplicate = param.get("duplicate", "none").strip() if duplicate is None: duplicate="none" if duplicate != "replace" and duplicate != "concat" and duplicate != "error" and duplicate != "none": raise ValueError("Invalid \"duplicate\" attribute in " + "parameter {0} found. Use \"replace\"" + ", \"concat\", \"error\" or \"none\" (default).".format(name)) if parameter_mode not in jube2.conf.ALLOWED_MODETYPES: raise ValueError( ("parameter-mode \"{0}\" not allowed in " + "").format(parameter_mode, name)) value_etree = param.find("value") if value_etree is not None: if value_etree.text is None: value = "" else: value = value_etree.text.strip() else: if param.text is None: value = "" else: value = param.text.strip() selection_etree = param.find("selection") if selection_etree is not None: selected_value = selection_etree.text if selected_value is None: selected_value = "" idx = int(selection_etree.get("idx", "-1")) else: selected_value = param.get("selection") idx = -1 if selected_value is not None: selected_value = selected_value.strip() parameter = \ jube2.parameter.Parameter.create_parameter( name, value, separator, parameter_type, selected_value, parameter_mode, parameter_unit, export, update_mode=parameter_update_mode, idx=idx, eval_helper=None, fixed=False, duplicate=duplicate) parameters.append(parameter) return parameters def _extract_patternsets(self, etree): """Return patternset from etree""" patternsets = dict() for element in etree.findall("patternset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None patternset = self._extract_extern_set(parts[0], "patternset", name, search_name) else: patternset = jube2.pattern.Patternset(name) for pattern in Parser._extract_pattern(element): patternset.add_pattern(pattern) if patternset.name in patternsets: raise ValueError("\"{0}\" not unique".format(patternset.name)) patternsets[patternset.name] = patternset return patternsets @staticmethod def _extract_pattern(etree_patternset): """Extract pattern from patternset Return a list of pattern""" patternlist = list() for pattern in etree_patternset: Parser._check_tag(pattern, ["pattern"]) name = Parser._attribute_from_element(pattern, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") if not re.match(r"^[^\d\W]\w*$", name, re.UNICODE): raise ValueError(("name=\"{0}\" in " + "contains a disallowed " + "character").format(name)) pattern_mode = pattern.get("mode", default="pattern").strip() if pattern_mode not in \ set(["pattern", "text"]).union( jube2.conf.ALLOWED_SCRIPTTYPES): raise ValueError(("pattern-mdoe \"{0}\" not allowed in " + "").format( pattern_mode, name)) content_type = pattern.get("type", default="string").strip() unit = pattern.get("unit", "").strip() dotall = \ pattern.get("dotall", "false").strip().lower() == "true" default = pattern.get("default") if default is not None: default = default.strip() if pattern.text is None: value = "" else: value = pattern.text.strip() patternlist.append(jube2.pattern.Pattern(name, value, pattern_mode, content_type, unit, default, dotall)) return patternlist def _extract_filesets(self, etree): """Return filesets from etree""" filesets = dict() for element in etree.findall("fileset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") filelist = Parser._extract_files(element) if name in filesets: raise ValueError("\"{0}\" not unique".format(name)) if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None filesets[name] = self._extract_extern_set(parts[0], "fileset", name, search_name) else: filesets[name] = jube2.fileset.Fileset(name) filesets[name] += filelist return filesets @staticmethod def _extract_files(etree_fileset): """Return filelist from fileset-etree""" filelist = list() valid_tags = ["copy", "link", "prepare"] for etree_file in etree_fileset: Parser._check_tag(etree_file, valid_tags) if etree_file.tag in ["copy", "link"]: separator = etree_file.get( "separator", jube2.conf.DEFAULT_SEPARATOR) source_dir = etree_file.get("directory", default="").strip() # New source_dir attribute overwrites deprecated directory # attribute source_dir_new = etree_file.get("source_dir") target_dir = etree_file.get("target_dir", default="").strip() if source_dir_new is not None: source_dir = source_dir_new.strip() active = etree_file.get("active", "true").strip() file_path_ref = etree_file.get("file_path_ref") alt_name = etree_file.get("name") # Check if the filepath is relativly seen to working dir or the # position of the xml-input-file is_internal_ref = \ etree_file.get("rel_path_ref", default="external").strip() == "internal" if etree_file.text is None: raise ValueError("Empty filelist in <{0}> found." .format(etree_file.tag)) files = jube2.util.util.safe_split(etree_file.text.strip(), separator) if alt_name is not None: # Use the new alternativ filenames names = [name.strip() for name in alt_name.split(jube2.conf.DEFAULT_SEPARATOR)] if len(names) != len(files): raise ValueError("Namelist and filelist must have " + "same length in <{0}>". format(etree_file.tag)) else: names = None for i, file_path in enumerate(files): path = file_path.strip() if names is not None: name = names[i] else: name = None if etree_file.tag == "copy": file_obj = jube2.fileset.Copy( path, name, is_internal_ref, active, source_dir, target_dir) elif etree_file.tag == "link": file_obj = jube2.fileset.Link( path, name, is_internal_ref, active, source_dir, target_dir) if file_path_ref is not None: file_obj.file_path_ref = \ os.path.expandvars(os.path.expanduser( file_path_ref.strip())) filelist.append(file_obj) elif etree_file.tag == "prepare": cmd = etree_file.text if cmd is None: cmd = "" cmd = cmd.strip() stdout_filename = etree_file.get("stdout") if stdout_filename is not None: stdout_filename = stdout_filename.strip() stderr_filename = etree_file.get("stderr") if stderr_filename is not None: stderr_filename = stderr_filename.strip() alt_work_dir = etree_file.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() active = etree_file.get("active", "true").strip() prepare_obj = jube2.fileset.Prepare(cmd, stdout_filename, stderr_filename, alt_work_dir, active) filelist.append(prepare_obj) return filelist def _extract_substitutesets(self, etree): """Extract substitutesets from benchmark Return a dict of substitute sets, e.g. {"compilesub": ([iofile0,...], [sub0,...])}""" substitutesets = dict() for element in etree.findall("substituteset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") files, subs = Parser._extract_subs(element) if name in substitutesets: raise ValueError("\"{0}\" not unique".format(name)) if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None substitutesets[name] = \ self._extract_extern_set(parts[0], "substituteset", name, search_name) substitutesets[name].update_files(files) substitutesets[name].update_substitute(subs) else: substitutesets[name] = \ jube2.substitute.Substituteset(name, files, subs) return substitutesets @staticmethod def _extract_subs(etree_substituteset): """Extract files for substitution and subs from substituteset Return a files dict for substitute and a dict of subs """ valid_tags = ["iofile", "sub"] files = list() subs = dict() for sub in etree_substituteset: Parser._check_tag(sub, valid_tags) if sub.tag == "iofile": in_file = Parser._attribute_from_element(sub, "in").strip() out_file = Parser._attribute_from_element( sub, "out").strip() out_mode = sub.get("out_mode", "w").strip() if out_mode not in ["w", "a"]: raise ValueError( "out_mode in must be \"w\" or \"a\"") in_file = os.path.expandvars(os.path.expanduser(in_file)) out_file = os.path.expandvars(os.path.expanduser(out_file)) files.append((out_file, in_file, out_mode)) elif sub.tag == "sub": source = "" + \ Parser._attribute_from_element(sub, "source").strip() if source == "": raise ValueError( "Empty \"source\" attribute in found.") dest = sub.get("dest") if dest is None: dest = sub.text if dest is None: dest = "" dest = dest.strip() + "" sub_type = sub.get("mode", default="text").strip() subs[source] = jube2.substitute.Sub(source, sub_type, dest) return (files, subs) @staticmethod def _attribute_from_element(element, attribute): """Return attribute from element element -- etree.Element attribute -- string Raise a useful exception if value not found """ value = element.get(attribute) if value is None: raise ValueError("Missing attribute '{0}' in <{1}>" .format(attribute, element.tag)) return value @staticmethod def _check_tag(element, valid_tags): """Check tag and raise a useful exception if needed element -- etree.Element valid_tags -- list of valid strings """ if element.tag not in valid_tags: raise ValueError(("Unknown tag or tag used in wrong " + "position:\n{0}").format( jube2.util.output.element_tree_tostring( element, encoding="UTF-8"))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/log.py0000664000174700017470000001301014603772010016565 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Logging Support""" from __future__ import (print_function, unicode_literals, division) import logging import sys import glob import os.path import jube2.conf class JubeLogger(logging.getLoggerClass(), object): """Overwrite logging to handle multi line messages.""" def _log(self, level, msg, *args, **kwargs): """Log multi line messages each as a separate entry.""" if hasattr(msg, "splitlines"): lines = msg.splitlines() else: lines = str(msg).splitlines() for line in lines: super(JubeLogger, self)._log(level, line, *args, **kwargs) logging.setLoggerClass(JubeLogger) LOGGING_MODE = jube2.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube2.conf.DEFAULT_LOGFILE_NAME CONSOLE_VERBOSE = False def get_logger(name=None): """Return logger given by name""" return logging.getLogger(name) def setup_logging(mode=None, filename=None, verbose=None): """Setup the logging configuration. Available modes are default log to console and file console only console output filename can be given optionally. verbose: enable verbose console output The setup includes setting the handlers and formatters. Calling this function multiple times causes old handlers to be removed before new ones are added. """ global LOGGING_MODE, LOGFILE_NAME, CONSOLE_VERBOSE # Use debug file name and debug file mode when in debug mode if jube2.conf.DEBUG_MODE: filename = jube2.conf.LOGFILE_DEBUG_NAME mode = "default" filemode = jube2.conf.LOGFILE_DEBUG_MODE else: filemode = "a" if mode is None: mode = LOGGING_MODE else: LOGGING_MODE = mode if filename is None: filename = LOGFILE_NAME else: LOGFILE_NAME = filename if verbose is None: verbose = CONSOLE_VERBOSE else: CONSOLE_VERBOSE = verbose # this is needed to make the other handlers accept on low priority # events _logger = get_logger("jube2") _logger.setLevel(logging.DEBUG) # list is needed since we remove from the list we just iterate # over for handler in list(_logger.handlers): handler.close() _logger.removeHandler(handler) # create, configure and add console handler console_formatter = logging.Formatter(jube2.conf.LOG_CONSOLE_FORMAT) console_handler = logging.StreamHandler(sys.stdout) if verbose: console_handler.setLevel(logging.DEBUG) else: console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_formatter) _logger.addHandler(console_handler) if mode == "default": try: # create, configure and add file handler file_formatter = logging.Formatter(jube2.conf.LOG_FILE_FORMAT) file_handler = logging.FileHandler(filename, filemode) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) _logger.addHandler(file_handler) except IOError: pass def search_for_logs(path=None): """Search for files matching in path with .log extension""" if path is None: path = "." matches = glob.glob(os.path.join(path, "*.log")) return matches def log_print(text): """Output text""" print(text) def matching_logs(commands, available_logs): """Find intersection between requested logs and available logs. Returns tuple (matching, not_matching), containing the intersection and its complement. Only compares basenames. """ requested_logs = set("{0}.log".format(command) for command in commands) matching = list() for log in available_logs: if os.path.basename(log) in requested_logs: matching.append(log) not_matching = requested_logs.difference(set([os.path.basename(log) for log in matching])) return matching, not_matching def safe_output_logfile(filename): """Try to print logfile. If try fails, fail gracefully.""" try: with open(filename) as logfile: log_print(logfile.read()) except IOError: log_print("No log found in current directory") def change_logfile_name(filename): """Change log file name if not in debug mode.""" if jube2.conf.DEBUG_MODE: return setup_logging(filename=filename, mode="default") def only_console_log(): """Change to console log if not in debug mode.""" if jube2.conf.DEBUG_MODE: return setup_logging(mode="console") def reset_logging(): """Reset logging to default.""" global LOGGING_MODE, LOGFILE_NAME LOGGING_MODE = jube2.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube2.conf.DEFAULT_LOGFILE_NAME ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/main.py0000664000174700017470000012474014603772010016745 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """CLI program""" from __future__ import (print_function, unicode_literals, division) import jube2.jubeio import jube2.util.util import jube2.util.output import jube2.conf import jube2.info import jube2.help import jube2.log import jube2.completion import sys import os import re import shutil from jube2.util.version import StrictVersion try: from urllib.request import urlopen except ImportError: from urllib import urlopen try: import argparse except ImportError: print("argparse module not available; either install it " "(https://pypi.python.org/pypi/argparse), or " "switch to a Python version that includes it.") sys.exit(1) LOGGER = jube2.log.get_logger(__name__) def continue_benchmarks(args): """Continue benchmarks""" found_benchmarks = search_for_benchmarks(args) jube2.conf.HIDE_ANIMATIONS = args.hide_animation for benchmark_folder in found_benchmarks: _continue_benchmark(benchmark_folder, args) def status(args): """Show benchmark status""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: benchmark = _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: return jube2.info.print_benchmark_status(benchmark) def output(args): """Show output filename""" found_workpackages = list() stdout_paths = set() stderr_paths = set() paths = set() found_workpackages = search_for_workpackage(args, True) for wp in found_workpackages: #create parameter dictionary param_dict = dict() #all parameter from the benchmark for param_set in wp.benchmark.parametersets.values(): for param in param_set.all_parameters: param_dict[param.name] = param.value #all jube parameter from the workpackage for param in wp.get_jube_parameterset(): param_dict[param.name] = param.value #all jube parameter from the benchmark for param in wp.benchmark.get_jube_parameterset(): param_dict[param.name] = param.value #create work directory if wp.step.alt_work_dir is None: work_dir = [os.getcwd(), wp.work_dir] else: dir_cache = jube2.util.util.substitution(wp.step.alt_work_dir, param_dict) dir_cache = os.path.expandvars(os.path.expanduser(dir_cache)) work_dir = [os.getcwd(), dir_cache] for operation in wp.step.operations: if operation.stdout_filename is not None: stdout_filename = jube2.util.util.substitution( operation.stdout_filename, param_dict) stdout_filename = \ os.path.expandvars(os.path.expanduser(stdout_filename)) else: stdout_filename = "stdout" work_dir.append(stdout_filename) stdout_paths.add(os.path.join(*work_dir)) work_dir.remove(stdout_filename) if operation.stderr_filename is not None: stderr_filename = jube2.util.util.substitution( operation.stderr_filename, param_dict) stderr_filename = \ os.path.expandvars(os.path.expanduser(stderr_filename)) else: stderr_filename = "stderr" work_dir.append(stderr_filename) stderr_paths.add(os.path.join(*work_dir)) work_dir.remove(stderr_filename) #show only error or done file if args.only: if args.only == "stdout": paths.update(stdout_paths) else: paths.update(stderr_paths) else: paths.update(stdout_paths) paths.update(stderr_paths) #sort paths paths = sorted(paths) #show conten of file, not only filename if args.display: for path in paths: LOGGER.info(path + "\n") file = open(path) LOGGER.info(file.read() + "\n") else: for path in paths: LOGGER.info(path + "\n") def benchmarks_results(args): """Show benchmark results""" found_benchmarks = search_for_benchmarks(args) result_list = list() # Start with the newest benchmark to set the newest result configuration found_benchmarks.reverse() cnt = 0 for benchmark_folder in found_benchmarks: if (args.num is None) or (cnt < args.num): result_list = _benchmark_result(benchmark_folder=benchmark_folder, args=args, result_list=result_list) cnt += 1 for result_data in result_list: result_data.create_result(reverse=args.reverse) def analyse_benchmarks(args): """Analyse benchmarks""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _analyse_benchmark(benchmark_folder, args) def remove_benchmarks(args): """Remove benchmarks or workpackages""" if(args.workpackage is not None): # If a workpackage id is provided by the user, only specific # workpackages will be removed found_workpackages = search_for_workpackage(args) for workpackage in found_workpackages: _remove_workpackage(workpackage, args) else: # Delete complete benchmarks found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _remove_benchmark(benchmark_folder, args) def command_help(args): """Show command help""" subparser = _get_args_parser()[1] if args.command is None: subparser["help"].print_help() elif args.command.lower() == "all": for key in sorted(jube2.help.HELP.keys()): print("{0}:".format(key)) print(jube2.help.HELP[key]) else: if args.command in jube2.help.HELP: if args.command in subparser: subparser[args.command].print_help() else: print(jube2.help.HELP[args.command]) else: print("no help found for {0}".format(args.command)) subparser["help"].print_help() def info(args): """Benchmark information""" if args.id is None: if args.step is not None or args.workpackage is not None: LOGGER.warning("The -s and -w options are ignored if no " "benchmark ID is given. Information for all " "benchmarks is printed out.") jube2.info.print_benchmarks_info(args.dir) else: found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: benchmark = \ _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: continue if args.step is None and args.workpackage is None: jube2.info.print_benchmark_info(benchmark) elif args.workpackage is None: # Display step information if args.step: steps = args.step else: steps = benchmark.steps.keys() # Set default csv_parametrization value to allow empty -c # option if args.csv_parametrization is None: args.csv_parametrization = "," for step_name in steps: jube2.info.print_step_info( benchmark, step_name, parametrization_only=args.parametrization, parametrization_only_csv=args.csv_parametrization) else: # Display workpackage information if args.step: steps = args.step else: steps = benchmark.steps.keys() if args.workpackage: wp_ids = [int(id) for id in args.workpackage] else: wp_ids = [wp.id for wps in benchmark.workpackages.values() for wp in wps] for wp_id in wp_ids: workpackage = benchmark.workpackage_by_id(wp_id) if workpackage: if workpackage.step.name in steps: jube2.info.print_workpackage_info(benchmark, workpackage) else: LOGGER.warning("Workpackage with ID is ignored for " "further execution. It was not found in " "the specified steps.".format(wp_id)) else: LOGGER.warning("Workpackage with ID is ignored for " "further execution. It was not found in " "the specified benchmark.".format(wp_id)) def update_check(args): """Check if a newer JUBE version is available.""" try: website = urlopen(jube2.conf.UPDATE_VERSION_URL) version = website.read().decode().strip() if StrictVersion(jube2.conf.JUBE_VERSION) >= StrictVersion(version): LOGGER.info("Newest JUBE version {0} is already " "installed.".format(jube2.conf.JUBE_VERSION)) else: LOGGER.info(("Newer JUBE version {0} is available. " "Currently installed version is {1}.\n" "New version can be " "downloaded here: {2}").format( version, jube2.conf.JUBE_VERSION, jube2.conf.UPDATE_URL)) except IOError as ioe: raise IOError("Cannot connect to {0}: {1}".format( jube2.conf.UPDATE_VERSION_URL, str(ioe))) except ValueError as verr: raise ValueError("Cannot read version string from {0}: {1}".format( jube2.conf.UPDATE_VERSION_URL, str(verr))) def show_log(args): """Show logs for benchmarks""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: show_log_single(args, benchmark_folder) def show_log_single(args, benchmark_folder): """Show logs for a single benchmark""" # Find available logs available_logs = jube2.log.search_for_logs(benchmark_folder) # Use all available logs if none is selected ... if not args.command: matching = available_logs not_matching = list() # ... otherwise find intersection between available and # selected else: matching, not_matching = jube2.log.matching_logs( args.command, available_logs) # Output the log file for log in matching: jube2.log.log_print("BenchmarkID: {0} | Log: {1}".format( int(os.path.basename(benchmark_folder)), log)) jube2.log.safe_output_logfile(log) # Inform user if any selected log was not found if not_matching: jube2.log.log_print("Could not find logs: {0}".format( ",".join(not_matching))) def complete(args): """Handle shell completion""" jube2.completion.complete_function_bash(args) def _load_existing_benchmark(args, benchmark_folder, restore_workpackages=True, load_analyse=True): """Load an existing benchmark, given by directory benchmark_folder.""" jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_PARSE_NAME)) # Add log information LOGGER.debug("Command: {0} {1}".format( os.path.basename(sys.argv[0]), " ".join(sys.argv[1:]))) LOGGER.debug("Version: {0}".format(jube2.conf.JUBE_VERSION)) # Read existing benchmark configuration try: parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.CONFIGURATION_FILENAME), force=args.force, strict=args.strict) benchmarks = parser.benchmarks_from_xml()[0] except IOError as exeption: LOGGER.warning(str(exeption)) return None # benchmarks can be None if version conflict was blocked if benchmarks is not None: # Only one single benchmark exist inside benchmarks benchmark = list(benchmarks.values())[0] else: return None # Restore old benchmark id benchmark.id = int(os.path.basename(benchmark_folder)) if restore_workpackages: # Read existing workpackage information try: parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.WORKPACKAGES_FILENAME), force=args.force, strict=args.strict) workpackages, work_stat = parser.workpackages_from_xml(benchmark) except IOError as exeption: LOGGER.warning(str(exeption)) return None benchmark.set_workpackage_information(workpackages, work_stat) if load_analyse and os.path.isfile(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME)): # Read existing analyse data parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME), force=args.force, strict=args.strict) analyse_result = parser.analyse_result_from_xml() if analyse_result is not None: for analyser in benchmark.analyser.values(): if analyser.name in analyse_result: analyser.analyse_result = analyse_result[analyser.name] jube2.log.only_console_log() return benchmark def manipulate_comments(args): """Manipulate benchmark comment""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _manipulate_comment(benchmark_folder, args) def search_for_benchmarks(args): """Search for existing benchmarks""" found_benchmarks = list() if not os.path.isdir(args.dir): raise OSError("Not a directory: \"{0}\"".format(args.dir)) all_benchmarks = [ os.path.join(args.dir, directory) for directory in os.listdir(args.dir) if os.path.isdir(os.path.join(args.dir, directory))] all_benchmarks.sort() if (args.id is not None) and ("all" not in args.id): for benchmark_id in args.id: if benchmark_id == "last": benchmark_id = jube2.util.util.get_current_id(args.dir) # Search for existing benchmark benchmark_id = int(benchmark_id) if benchmark_id < 0: benchmark_id = int( os.path.basename(all_benchmarks[benchmark_id])) benchmark_folder = jube2.util.util.id_dir(args.dir, benchmark_id) if not os.path.isdir(benchmark_folder): raise OSError("Benchmark directory not found: \"{0}\"" .format(benchmark_folder)) if not os.path.isfile(os.path.join( benchmark_folder, jube2.conf.CONFIGURATION_FILENAME)): LOGGER.warning(("Configuration file \"{0}\" not found in " + "\"{1}\" or directory not readable.") .format(jube2.conf.CONFIGURATION_FILENAME, benchmark_folder)) if benchmark_folder not in found_benchmarks: found_benchmarks.append(benchmark_folder) else: if (args.id is not None) and ("all" in args.id): # Add all available benchmark folder found_benchmarks = all_benchmarks else: # Get highest benchmark id and build benchmark_folder benchmark_id = jube2.util.util.get_current_id(args.dir) benchmark_folder = jube2.util.util.id_dir(args.dir, benchmark_id) if os.path.isdir(benchmark_folder): found_benchmarks.append(benchmark_folder) else: raise OSError("No benchmark directory found in \"{0}\"" .format(args.dir)) found_benchmarks = \ [benchmark_folder for benchmark_folder in found_benchmarks if os.path.isfile(os.path.join(benchmark_folder, jube2.conf.CONFIGURATION_FILENAME))] found_benchmarks.sort() return found_benchmarks def search_for_workpackage(args, search_for_step=False): """Search for existing workpackages""" found_benchmarks = search_for_benchmarks(args) found_workpackages = list() for benchmark_folder in found_benchmarks: benchmark = \ _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is not None: if args.workpackage: for wp_id in args.workpackage: if search_for_step and args.step: LOGGER.warning("The '-s' option is ignored if " "workpackages are selected by their ID " "using '-w'.") if benchmark.workpackage_by_id(int(wp_id)) is None: raise RuntimeError(("Workpackage ID \"{0}\" not " + "found in benchmark \"{1}\".") .format(wp_id, benchmark.id)) else: found_workpackages.append( benchmark.workpackage_by_id(int(wp_id))) elif search_for_step and args.step: for step_name in args.step: if step_name not in benchmark.workpackages: LOGGER.warning("Step \"{0}\" not found in benchmark " "\"{1}\".".format(step_name, benchmark.name)) else: for wp in benchmark.workpackages[step_name]: found_workpackages.append(wp) elif search_for_step: for wp_name in benchmark.workpackages: for wp in benchmark.workpackages[wp_name]: found_workpackages.append(wp) return found_workpackages def run_new_benchmark(args): """Start a new benchmark run""" jube2.conf.HIDE_ANIMATIONS = args.hide_animation jube2.conf.EXIT_ON_ERROR = args.error id_cnt = 0 # Extract tags tags = args.tag if tags is not None: tags = set(tags) for path in args.files: # Setup Logging jube2.log.change_logfile_name( filename=os.path.join(os.path.dirname(path), jube2.conf.DEFAULT_LOGFILE_NAME)) # Add log information LOGGER.debug("Command: {0} {1}".format( os.path.basename(sys.argv[0]), " ".join(sys.argv[1:]))) LOGGER.debug("Version: {0}".format(jube2.conf.JUBE_VERSION)) # Read new benchmarks if args.include_path is not None: include_pathes = [include_path for include_path in args.include_path if include_path != ""] else: include_pathes = None parser = jube2.jubeio.Parser(path, tags, include_pathes, args.force, args.strict) benchmarks, only_bench, not_bench = parser.benchmarks_from_xml() # Add new comment if args.comment is not None: for benchmark in benchmarks.values(): benchmark.comment = re.sub(r"\s+", " ", args.comment) # CLI input overwrite fileinput if args.only_bench: only_bench = args.only_bench if args.not_bench: not_bench = args.not_bench # No specific -> do all if len(only_bench) == 0 and benchmarks is not None: only_bench = list(benchmarks) for bench_name in only_bench: if bench_name in not_bench: continue bench = benchmarks[bench_name] # Set user defined id if (args.id is not None) and (len(args.id) > id_cnt): if args.id[id_cnt] < 0: LOGGER.warning("Negative ids are not allowed. Skipping id " "'{}'.".format(args.id[id_cnt])) id_cnt += 1 continue bench.id = args.id[id_cnt] id_cnt += 1 # Change runtime outpath if specified if args.outpath is not None: bench.outpath = args.outpath # Start benchmark run bench.new_run() # Run analyse if args.analyse or args.result: jube2.log.change_logfile_name(os.path.join( bench.bench_dir, jube2.conf.LOGFILE_ANALYSE_NAME)) bench.analyse() # Create result data if args.result: jube2.log.change_logfile_name(os.path.join( bench.bench_dir, jube2.conf.LOGFILE_RESULT_NAME)) bench.create_result(show=True) # Clean up when using debug mode if jube2.conf.DEBUG_MODE: bench.delete_bench_dir() # Reset logging jube2.log.only_console_log() def _continue_benchmark(benchmark_folder, args): """Continue existing benchmark""" jube2.conf.EXIT_ON_ERROR = args.error benchmark = _load_existing_benchmark(args, benchmark_folder) if benchmark is None: return # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_CONTINUE_NAME)) # Run existing benchmark benchmark.run() # Run analyse if args.analyse or args.result: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse() # Create result data if args.result: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_RESULT_NAME)) benchmark.create_result(show=True) # Clean up when using debug mode if jube2.conf.DEBUG_MODE: benchmark.reset_all_workpackages() # Reset logging jube2.log.only_console_log() def _analyse_benchmark(benchmark_folder, args): """Analyse existing benchmark""" benchmark = _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: return # Update benchmark data _update_analyse_and_result(args, benchmark) # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) LOGGER.info(jube2.util.output.text_boxed( ("Analyse benchmark \"{0}\" id: {1}").format(benchmark.name, benchmark.id))) benchmark.analyse() if os.path.isfile( os.path.join(benchmark_folder, jube2.conf.ANALYSE_FILENAME)): LOGGER.info(">>> Analyse data storage: {0}".format(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME))) else: LOGGER.info(">>> Analyse data storage \"{0}\" not created!".format( os.path.join(benchmark_folder, jube2.conf.ANALYSE_FILENAME))) LOGGER.info(jube2.util.output.text_line()) # Reset logging jube2.log.only_console_log() def _benchmark_result(benchmark_folder, args, result_list=None): """Show benchmark result""" benchmark = _load_existing_benchmark(args, benchmark_folder) if result_list is None: result_list = list() if benchmark is None: return result_list if (args.update is None) and (args.tag is not None) and \ (len(benchmark.tags & set(args.tag)) == 0): return result_list # Update benchmark data _update_analyse_and_result(args, benchmark) # Run benchmark analyse if args.analyse: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse(show_info=False) # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_RESULT_NAME)) # Create benchmark results result_list = benchmark.create_result(only=args.only, data_list=result_list, style=args.style, select=args.select, exclude=args.exclude) # Reset logging jube2.log.only_console_log() return result_list def _update_analyse_and_result(args, benchmark): """Update analyse and result data in given benchmark by using the given update file""" if args.update is not None: dirname = os.path.dirname(args.update) # Extract tags benchmark.add_tags(args.tag) tags = benchmark.tags # Read new benchmarks if args.include_path is not None: include_pathes = [include_path for include_path in args.include_path if include_path != ""] else: include_pathes = None parser = jube2.jubeio.Parser(args.update, tags, include_pathes, args.force, args.strict) benchmarks = parser.benchmarks_from_xml()[0] # Update benchmark for bench in benchmarks.values(): if bench.name == benchmark.name: benchmark.update_analyse_and_result(bench.patternsets, bench.analyser, bench.results, bench.results_order, dirname) break else: LOGGER.debug(("No benchmark data for benchmark {0} was found " + "while running update.").format(benchmark.name)) def _remove_benchmark(benchmark_folder, args): """Remove existing benchmark""" remove = True if not args.force: try: inp = raw_input("Really remove \"{0}\" (y/n):" .format(benchmark_folder)) except NameError: inp = input("Really remove \"{0}\" (y/n):" .format(benchmark_folder)) remove = inp.startswith("y") if remove: # Delete benchmark folder shutil.rmtree(benchmark_folder, ignore_errors=True) def _remove_workpackage(workpackage, args): """Remove existing workpackages""" remove = True # Ignore deleted/unstarted workpackages if workpackage.started: if not args.force: try: inp = raw_input(("Really remove \"{0}\" and its dependent " + "workpackages (y/n):") .format(workpackage.workpackage_dir)) except NameError: inp = input(("Really remove \"{0}\" and its dependent " + "workpackages (y/n):") .format(workpackage.workpackage_dir)) remove = inp.startswith("y") if remove: workpackage.remove() workpackage.benchmark.write_workpackage_information( os.path.join(workpackage.benchmark.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) def _manipulate_comment(benchmark_folder, args): """Change or append the comment in given benchmark.""" benchmark = _load_existing_benchmark(args, benchmark_folder=benchmark_folder, restore_workpackages=False, load_analyse=False) if benchmark is None: return # Change benchmark comment if args.append: comment = benchmark.comment + args.comment else: comment = args.comment benchmark.comment = re.sub(r"\s+", " ", comment) benchmark.write_benchmark_configuration( os.path.join(benchmark_folder, jube2.conf.CONFIGURATION_FILENAME), outpath="..") def gen_parser_conf(): """Generate dict with parser information""" config = ( (("-V", "--version"), {"help": "show version", "action": "version", "version": "JUBE, version {0}".format( jube2.conf.JUBE_VERSION)}), (("-v", "--verbose"), {"help": "enable verbose console output (use -vv to " + "show stdout during execution and -vvv to " + "show log and stdout)", "action": "count", "default": 0}), (("--debug",), {"action": "store_true", "help": 'use debugging mode'}), (("--force",), {"action": "store_true", "help": 'skip version check'}), (("--strict",), {"action": "store_true", "help": 'force need for correct version'}), (("--devel",), {"action": "store_true", "help": 'show development related information'}) ) return config def gen_subparser_conf(): """Generate dict with subparser information""" subparser_configuration = dict() # run subparser subparser_configuration["run"] = { "help": "processes benchmark", "func": run_new_benchmark, "arguments": { ("files",): {"metavar": "FILE", "nargs": "+", "help": "input file"}, ("--only-bench",): {"nargs": "+", "help": "only run benchmark"}, ("--not-bench",): {"nargs": "+", "help": "do not run benchmark"}, ("-t", "--tag"): {"nargs": "+", "help": "select tags"}, ("-i", "--id"): {"type": int, "help": "use specific benchmark id", "nargs": "+"}, ("-e", "--error"): {"action": "store_true", "help": "exit on error"}, ("--hide-animation",): {"action": "store_true", "help": "hide animations"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse"}, ("-r", "--result"): {"action": "store_true", "help": "show results"}, ("-m", "--comment"): {"help": "add comment"}, ("-o", "--outpath"): {"help": "overwrite outpath directory"} } } # continue subparser subparser_configuration["continue"] = { "help": "continue benchmark", "func": continue_benchmarks, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("--hide-animation",): {"action": "store_true", "help": "hide animations"}, ("-e", "--error"): {"action": "store_true", "help": "exit on error"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse"}, ("-r", "--result"): {"action": "store_true", "help": "show results"} } } # analyse subparser subparser_configuration["analyse"] = { "help": "analyse benchmark", "func": analyse_benchmarks, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-u", "--update"): {"metavar": "UPDATE_FILE", "help": "update analyse and result configuration"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-t", "--tag"): {"nargs": "+", "help": "select tags"} } } # result subparser subparser_configuration["result"] = { "help": "show benchmark results", "func": benchmarks_results, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse before creating result"}, ("-u", "--update"): {"metavar": "UPDATE_FILE", "help": "update analyse and result configuration"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-t", "--tag"): {"nargs": '+', "help": "select tags"}, ("-o", "--only"): {"nargs": "+", "metavar": "RESULT_NAME", "help": "only create results given by specific name"}, ("-r", "--reverse"): {"help": "reverse benchmark output order", "action": "store_true"}, ("-n", "--num"): {"type": int, "help": "show only last N benchmarks"}, ("-s", "--style"): {"help": "overwrites table style type", "choices": ["pretty", "csv", "aligned"]}, ("--select",): {"nargs": "+", "help": "display only given columns from the result " "(changes also the output to the result file)"}, ("--exclude",): {"nargs": "+", "help": "excludes given columns from the result " "(changes also the output to the result file)"} } } # info subparser subparser_configuration["info"] = { "help": "benchmark information", "func": info, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-s", "--step"): {"help": "show information for given step", "nargs": "*"}, ("-p", "--parametrization"): {"help": "display only parametrization of given step", "action": "store_true"}, ("-c", "--csv-parametrization"): {"help": "display only parametrization of given step " + "using csv format", "nargs": "?", "default": False, "metavar": "SEPARATOR"}, ("-w", "--workpackage"): {"help": "show information for given workpackage id", "nargs": "*"} } } # status subparser subparser_configuration["status"] = { "help": "show benchmark status", "func": status, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"} } } #output subparser subparser_configuration["output"] = { "help": "show filename of output", "func": output, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-s", "--step"): {"help": "show filenames for given step", "nargs": "+"}, ("-w", "--workpackage"): {"help": "show filenames for given workpackages id", "nargs": "+"}, ("-d", "--display"): {"help": "display content of output file" , "action": "store_true"}, ("-o", "--only"): {"help": "show only stdour or stderr", "choices": ["stdout", "stderr"]} } } # comment subparser subparser_configuration["comment"] = { "help": "comment handling", "func": manipulate_comments, "arguments": { ('comment',): {"help": "comment"}, ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-a", "--append"): {"help": "append comment to existing one", "action": 'store_true'} } } # remove subparser subparser_configuration["remove"] = { "help": "remove benchmark or workpackages", "func": remove_benchmarks, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "remove benchmarks given by id", "nargs": "+"}, ("-w", "--workpackage"): {"help": "specifc workpackage id to be removed", "nargs": "+"}, ("-f", "--force"): {"help": "force removing, never prompt", "action": "store_true"} } } # update subparser subparser_configuration["update"] = { "help": "Check if a newer JUBE version is available", "func": update_check } # log subparser subparser_configuration["log"] = { "help": "show benchmark logs", "func": show_log, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ('--command', "-c"): {"nargs": "+", "help": "show log for this command"}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"} } } # completion subparser subparser_configuration["complete"] = { "help": "generate shell completion ", "func": complete, "arguments": { ('--command-name', "-c"): {"nargs": 1, "help": "name of command to be completed", "default": [os.path.basename(sys.argv[0])]}, } } return subparser_configuration def _get_args_parser(): """Create argument parser""" parser = argparse.ArgumentParser() for args, kwargs in gen_parser_conf(): parser.add_argument(*args, **kwargs) subparsers = parser.add_subparsers(dest="subparser", help='subparsers') subparser_configuration = gen_subparser_conf() # create subparser out of subparser configuration subparser = dict() for name, subparser_config in subparser_configuration.items(): subparser[name] = \ subparsers.add_parser( name, help=subparser_config.get("help", ""), description=jube2.help.HELP.get(name, ""), formatter_class=argparse.RawDescriptionHelpFormatter) subparser[name].set_defaults(func=subparser_config["func"]) if "arguments" in subparser_config: for names, arg in subparser_config["arguments"].items(): subparser[name].add_argument(*names, **arg) # create help key word overview help_keys = sorted(list(jube2.help.HELP) + ["ALL"]) max_word_length = max(map(len, help_keys)) + 4 # calculate max number of keyword columns max_columns = jube2.conf.DEFAULT_WIDTH // max_word_length # fill keyword list to match number of columns help_keys += [""] * (len(help_keys) % max_columns) help_keys = list(zip(*[iter(help_keys)] * max_columns)) # create overview help_overview = jube2.util.output.text_table(help_keys, separator=" ", align_right=False) # help subparser subparser["help"] = \ subparsers.add_parser( 'help', help='command help', formatter_class=argparse.RawDescriptionHelpFormatter, description="available commands or info elements: \n" + help_overview) subparser["help"].add_argument('command', nargs='?', help="command or info element") subparser["help"].set_defaults(func=command_help) return parser, subparser def main(command=None): """Parse the command line and run the requested command.""" jube2.help.load_help() parser = _get_args_parser()[0] if command is None: args = parser.parse_args() else: args = parser.parse_args(command) jube2.conf.DEBUG_MODE = args.debug jube2.conf.VERBOSE_LEVEL = args.verbose if jube2.conf.VERBOSE_LEVEL > 0: args.hide_animation = True # Set new umask if JUBE_GROUP_NAME is used current_mask = os.umask(0) if (jube2.util.util.check_and_get_group_id() is not None) and \ (current_mask > 2): current_mask = 2 os.umask(current_mask) if args.subparser: jube2.log.setup_logging(mode="console", verbose=(jube2.conf.VERBOSE_LEVEL == 1) or (jube2.conf.VERBOSE_LEVEL == 3)) if args.devel: args.func(args) else: try: args.func(args) except Exception as exeption: # Catch all possible Exceptions LOGGER.error("\n" + str(exeption)) jube2.log.reset_logging() exit(1) else: parser.print_usage() jube2.log.reset_logging() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/parameter.py0000664000174700017470000011042214603772010017771 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Parameter related classes""" from __future__ import (print_function, unicode_literals, division) import itertools import os import xml.etree.ElementTree as ET import copy import jube2.util.util import jube2.conf import jube2.log import re import inspect LOGGER = jube2.log.get_logger(__name__) JUBE_MODE = "jube" NEVER_MODE = "never" STEP_MODE = "step" CYCLE_MODE = "cycle" ALWAYS_MODE = "always" USE_MODE = "use" UPDATE_MODES = (JUBE_MODE, NEVER_MODE, STEP_MODE, CYCLE_MODE, USE_MODE, ALWAYS_MODE) class Parameterset(object): """A parameterset represent a template or a specific product space. It can be combined with other Parametersets.""" def __init__(self, name="", duplicate="replace"): self._name = name self._duplicate = duplicate self._parameters = dict() def clear(self): """Remove all stored parameters""" self._parameters = dict() def copy(self): """Returns a deepcopy of the Parameterset""" new_parameterset = Parameterset(self._name, self._duplicate) new_parameterset.add_parameterset(self) return new_parameterset @property def name(self): """Return name of the Parameterset""" return self._name @property def duplicate(self): """Return the duplicate property of the Parameterset""" return self._duplicate @property def has_templates(self): """This Parameterset contains template paramters?""" for parameter in self._parameters.values(): if parameter.is_template: return True return False @property def parameter_dict(self): """Return dictionary name -> parameter""" return dict(self._parameters) @property def all_parameters(self): """Return list of all parameters""" return self._parameters.values() @property def all_parameter_names(self): """Return list of all parameter names""" return self._parameters.keys() def add_parameterset(self, parameterset): """Add all parameters from given parameterset, existing ones will be overwritten""" for parameter in parameterset: self.add_parameter(parameter.copy()) return self def update_parameterset(self, parameterset): """Overwrite existing parameters. Do not add new parameters""" for parameter in parameterset: if parameter.name in self: self._parameters[parameter.name] = parameter.copy() def concat_parameter(self, parameter): """Concatenate a new parameter to a potentially existing one.""" if parameter.name in self._parameters.keys(): if self._parameters[parameter.name]._value == parameter._value: return parameter else: value=list(set(jube2.util.util.ensure_list(self._parameters[parameter.name]._value)+jube2.util.util.ensure_list(parameter._value))) value.sort() return jube2.parameter.TemplateParameter( parameter._name, value, parameter._separator, parameter._type, parameter._mode, parameter._unit, parameter._export, parameter._update_mode, parameter._idx, parameter._eval_helper, parameter._duplicate) else: return parameter def check_parameter_options(self, parameter, only_duplicate=True): """Check whether both parameters have identical options and throw an error if this is not the case""" if only_duplicate: if parameter._duplicate != self._parameters[parameter.name]._duplicate: LOGGER.debug( "The duplicate options for the parameter {0} are stated at least twice differently leading to undefined behaviour.\n".format( parameter.name)) raise ValueError("The duplicate options for the parameter {0} are stated at least twice differently leading to undefined behaviour.".format(parameter.name)) else: if parameter._separator != self._parameters[parameter.name]._separator or \ parameter._type != self._parameters[parameter.name]._type or \ parameter._update_mode != self._parameters[parameter.name]._update_mode: LOGGER.debug( "At least one option (separator, type, update_mode) for the parameter {0} was defined at least twice differently leading to undefined behaviour.\n".format( parameter.name)) raise ValueError("At least one option (separator, type, update_mode) for the parameter {0} was defined at least twice differently leading to undefined behaviour.".format(parameter.name)) def add_parameter(self, parameter): """Add a new parameter""" if parameter.name not in self._parameters.keys(): self._parameters[parameter.name] = parameter else: # Check whether only the duplicate option of two parameters is # identical, otherwise the behaviour is undefined. self.check_parameter_options(parameter=parameter, only_duplicate=True) # check, which action to perform and prioritize the duplicate # option from the parameters over the duplicate option from # the parametersets raise_unknown_error=False if parameter._duplicate == "replace": self._parameters[parameter.name] = parameter elif parameter._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif parameter._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter elif parameter._duplicate == "none": if self._duplicate == "replace": self._parameters[parameter.name] = parameter elif self._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif self._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter else: # unknown error, this situation should never occur! raise_unknown_error=True else: # unknown error, this situation should never occur! raise_unknown_error=True if raise_unknown_error: raise Exception("The execution was aborted due to an unknown error "+ "when adding a parameter. Please contact the JUBE developers "+ "to resolve this situation.") def delete_parameter(self, parameter): """Delete a parameter""" name = "" if isinstance(parameter, Parameter): name = parameter.name else: name = parameter if name in self._parameters: del self._parameters[name] @property def constant_parameter_dict(self): """Return dictionary representation of all constant parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and (parameter.mode not in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES))]) @property def template_parameter_dict(self): """Return dictionary representation of all template parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if parameter.is_template]) @property def export_parameter_dict(self): """Return dictionary representation of all export parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and parameter.export]) def get_updatable_parameter(self, mode, keep_index=False): """Returns a parameterset containing all updatable parameter for a specific mode, the root parameter is added""" parameterset = Parameterset() for parameter in self._parameters.values(): if ((parameter.update_mode == mode) or (parameter.update_mode == ALWAYS_MODE and mode == CYCLE_MODE) or (parameter.update_mode == STEP_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == STEP_MODE)): root_paramter = parameter.based_on_root.copy() if keep_index: root_paramter.idx = parameter.idx parameterset.add_parameter(root_paramter) return parameterset def is_compatible(self, parameterset, update_mode=NEVER_MODE): """Two Parametersets are compatible, if the intersection only contains equivilant parameters""" return len(self.get_incompatible_parameter( parameterset, update_mode)) == 0 def get_incompatible_parameter(self, parameterset, update_mode=NEVER_MODE): """Return a set of incompatible parameter names between the current and the given parameterset""" result = set() # Find parameternames which exists in both parametersets intersection = set(self.all_parameter_names) & \ set(parameterset.all_parameter_names) for name in intersection: if (not (self[name].update_allowed(update_mode) or # In case of the USE_MODE (in the beginning of a # new step) only the actual new parameterset and its # mode is relevant parameterset[name].update_allowed( NEVER_MODE if (update_mode == USE_MODE) else update_mode)) and not self[name].is_equivalent(parameterset[name])): result.add(name) return result def remove_jube_parameter(self): """Remove JUBE update mode parameter from the parameterset""" remove_list = [] for parameter in self: if parameter.is_jube_parameter: remove_list.append(parameter.name) for parameter_name in remove_list: self.delete_parameter(parameter_name) def expand_templates(self): """Expand all remaining templates in the Parameterset and returns the resulting parametersets """ parameter_list = list() # Create all possible constant parameter representations for parameter in self.template_parameter_dict.values(): expanded_parameter_list = list() for static_param in parameter.expand(): expanded_parameter_list.append(static_param) parameter_list.append(expanded_parameter_list) # Generator for parameters in itertools.product(*parameter_list): parameterset = self.copy() # Addition of the constant parameters will overwrite the templates for parameter in parameters: parameterset.add_parameter(parameter) yield parameterset def __contains__(self, parameter): if isinstance(parameter, Parameter): if parameter.name in self._parameters: return parameter.is_equivalent( self._parameters[parameter.name]) else: return False else: return parameter in self._parameters def __getitem__(self, name): if name in self._parameters: return self._parameters[name] else: return None def __iter__(self): for parameter in self.all_parameters: yield parameter def etree_repr(self, use_current_selection=False): """Return etree object representation""" parameterset_etree = ET.Element('parameterset') if len(self._name) > 0: parameterset_etree.attrib["name"] = self._name parameterset_etree.attrib["duplicate"] = self._duplicate for parameter in self._parameters.values(): parameterset_etree.append( parameter.etree_repr(use_current_selection)) return parameterset_etree def __len__(self): return len(self._parameters) def __repr__(self): return "Parameterset:{0}".format( dict([[parameter.name, parameter.value] for parameter in self.all_parameters])) def parameter_substitution(self, additional_parametersets=None, final_sub=False): """Substitute all parameter inside the parameterset. Parameters from additional_parameterset will be used for substitution but will not be added to the set. final_sub marks the last substitution process.""" set_changed = True count = 0 while set_changed and (not self.has_templates) and \ (count < jube2.conf.MAX_RECURSIVE_SUB): set_changed = False count += 1 # Create dependencies depend_dict = dict() for par in self: if not par.is_template: depend_dict[par.name] = set() for other_par in self: # search for parameter usage if par.depends_on(other_par): depend_dict[par.name].add(other_par.name) # Resolve dependencies substitution_list = [self._parameters[name] for name in jube2.util.util.resolve_depend(depend_dict)] # Do substition and evaluation if possible set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets) # Run forced evaluation if there were no further changes if not set_changed: set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets, force_evaluation=True) if final_sub: parameter = [par for par in self] for par in parameter: if par.is_template: LOGGER.debug( ("Parameter ${0} = {1} is handled as " + "a template and will not be evaluated.\n").format( par.name, par.value)) else: new_par, param_changed = \ par.substitute_and_evaluate(final_sub=True) if param_changed: self.add_parameter(new_par) def __substitute_parameters_in_list(self, parameter_list, additional_parametersets=None, force_evaluation=False): """Substitute all parameter inside the given parameter_list. Parameters from additional_parameterset will be used for substitution but will not be added to the set. force_evaluation will force script parameter evaluation""" set_changed = False for par in parameter_list: if par.can_substitute_and_evaluate(self): parametersets = [self] if additional_parametersets is not None: parametersets += additional_parametersets new_par, param_changed = \ par.substitute_and_evaluate( parametersets, force_evaluation=force_evaluation) if param_changed: self.add_parameter(new_par) set_changed = set_changed or param_changed return set_changed class Parameter(object): """Contains data for single Parameter. This Parameter can be a constant value, a template or a specific value out of a given template""" # This regex can be used to find variables inside parameter values parameter_regex = \ re.compile(r"(?. """Patternset definition""" from __future__ import (print_function, unicode_literals, division) import jube2.parameter import xml.etree.ElementTree as ET LOGGER = jube2.log.get_logger(__name__) class Patternset(object): """A Patternset stores a set of pattern and derived pattern.""" def __init__(self, name=""): self._name = name self._pattern = jube2.parameter.Parameterset("pattern") self._derived_pattern = jube2.parameter.Parameterset("derived_pattern") def add_pattern(self, pattern): """Add a additional pattern to the patternset. Existing pattern using the same name will be overwritten""" if pattern.derived: if pattern in self._pattern: self._pattern.delete_parameter(pattern) self._derived_pattern.add_parameter(pattern) else: if pattern in self._derived_pattern: self._derived_pattern.delete_parameter(pattern) self._pattern.add_parameter(pattern) @property def pattern_storage(self): """Return the pattern storage""" return self._pattern @property def derived_pattern_storage(self): """Return the derived pattern storage""" return self._derived_pattern def etree_repr(self): """Return etree object representation""" patternset_etree = ET.Element('patternset') patternset_etree.attrib["name"] = self._name for pattern in self._pattern: patternset_etree.append( pattern.etree_repr()) for pattern in self._derived_pattern: patternset_etree.append( pattern.etree_repr()) return patternset_etree def add_patternset(self, patternset): """Add all pattern from given patternset to the current one""" self._pattern.add_parameterset(patternset.pattern_storage) self._derived_pattern.add_parameterset( patternset.derived_pattern_storage) def pattern_substitution(self, parametersets=None): """Run pattern substitution using additional parameterset""" if parametersets is None: parametersets = list() self._pattern.parameter_substitution( additional_parametersets=parametersets, final_sub=True) def derived_pattern_substitution(self, parametersets=None): """Run derived pattern substitution using additional parameterset""" if parametersets is None: parametersets = list() self._derived_pattern.parameter_substitution( additional_parametersets=parametersets, final_sub=True) @property def name(self): """Get patternset name""" return self._name def copy(self): """Returns a copy of the Parameterset""" new_patternset = Patternset(self._name) new_patternset.add_patternset(self) return new_patternset def is_compatible(self, patternset): """Two Patternsets are compatible, if all pattern storages are compatible""" return self.pattern_storage.is_compatible( patternset.pattern_storage) and \ self.pattern_storage.is_compatible( patternset.derived_pattern_storage) and \ self.derived_pattern_storage.is_compatible( patternset.derived_pattern_storage) and \ self.derived_pattern_storage.is_compatible( patternset.pattern_storage) def get_incompatible_pattern(self, patternset): """Return a set of incompatible pattern names between the current and the given parameterset""" result = set() result.update(self.pattern_storage.get_incompatible_parameter( patternset.pattern_storage)) result.update(self.pattern_storage.get_incompatible_parameter( patternset.derived_pattern_storage)) result.update(self.derived_pattern_storage.get_incompatible_parameter( patternset.pattern_storage)) result.update(self.derived_pattern_storage.get_incompatible_parameter( patternset.derived_pattern_storage)) return result def __repr__(self): return "Patternset: pattern:{0} derived pattern:{1}".format( dict([[pattern.name, pattern.value] for pattern in self._pattern]), dict([[pattern.name, pattern.value] for pattern in self._derived_pattern])) def __contains__(self, pattern): if isinstance(pattern, Pattern): if pattern.name in self._pattern: return pattern.is_equivalent( self._pattern[pattern.name]) elif pattern.name in self._derived_pattern: return pattern.is_equivalent( self._derived_pattern[pattern.name]) else: return False else: return (pattern in self._pattern) or \ (pattern in self._derived_pattern) def __getitem__(self, name): """Returns pattern given by name. Is pattern not found, None will be returned""" if name in self._pattern: return self._pattern[name] elif name in self._derived_pattern: return self._derived_pattern[name] else: return None class Pattern(jube2.parameter.StaticParameter): """A pattern can be used to scan a result file, using regular expression, or to represent a derived pattern.""" def __init__(self, name, value, pattern_mode="pattern", content_type="string", unit="", default=None, dotall=False): self._derived = pattern_mode != "pattern" if not self._derived: pattern_mode = "text" self._default = default self._dotall = dotall # Unicode conversion value = "" + value self._unit = unit jube2.parameter.StaticParameter.__init__( self, name, value, parameter_type=content_type, parameter_mode=pattern_mode, unit=self._unit) @property def derived(self): """pattern is a derived pattern""" return self._derived @property def content_type(self): """Return pattern type""" return self._type @property def default_value(self): """Return pattern default value""" return self._default @property def dotall(self): """Return pattern dot regex handling""" return self._dotall @property def unit(self): """Return unit""" return self._unit def substitute_and_evaluate(self, parametersets=None, final_sub=False, no_templates=True, force_evaluation=False): """Substitute all variables inside the pattern value by using the parameter inside the given parameterset and additional_parameterset. final_sub marks the last substitution. Return the new pattern and a boolean value which represent a change of value """ try: # To take care of default values for derived pattern sets, always # run final_sub instead of force_evaluation. Otherwise no error # will be thrown. Only using the final_sub setup is too late # because the default pattern might be used within another derived # pattern if (self._mode in jube2.conf.ALLOWED_SCRIPTTYPES and force_evaluation and self._default is not None): final_sub = True force_evaluation = False param, changed = \ jube2.parameter.StaticParameter.substitute_and_evaluate( self, parametersets, final_sub, no_templates, force_evaluation) except RuntimeError as re: LOGGER.debug(str(re).replace("parameter", "pattern")) if self._default is not None: value = self._default elif self._type in ["int", "float"]: value = "nan" else: value = "" pattern = Pattern( self._name, value, "text", self._type, self._unit, dotall=self._dotall) pattern.based_on = self return pattern, True if changed: # Convert parameter to pattern if not self.derived: pattern_mode = "pattern" else: pattern_mode = param.mode pattern = Pattern(param.name, param.value, pattern_mode, param.parameter_type, self._unit, dotall=self._dotall) pattern.based_on = param.based_on else: pattern = param return pattern, changed def etree_repr(self, use_current_selection=False): """Return etree object representation""" pattern_etree = ET.Element('pattern') pattern_etree.attrib["name"] = self._name pattern_etree.attrib["type"] = self._type pattern_etree.attrib["dotall"] = str(self._dotall) if self._default is not None: pattern_etree.attrib["default"] = self._default if not self._derived: pattern_etree.attrib["mode"] = "pattern" else: pattern_etree.attrib["mode"] = self._mode if self._unit != "": pattern_etree.attrib["unit"] = self._unit pattern_etree.text = self.value return pattern_etree def __repr__(self): return "Pattern({0})".format(self.__dict__) def get_jube_pattern(): """Return jube internal patternset""" patternset = Patternset() # Pattern for integer number patternset.add_pattern(Pattern("jube_pat_int", r"([+-]?\d+)")) # Pattern for integer number, no () patternset.add_pattern(Pattern("jube_pat_nint", r"(?:[+-]?\d+)")) # Pattern for floating point number patternset.add_pattern( Pattern("jube_pat_fp", r"([+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))")) # Pattern for floating point number, no () patternset.add_pattern( Pattern("jube_pat_nfp", r"(?:[+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))")) # Pattern for word (all noblank characters) patternset.add_pattern(Pattern("jube_pat_wrd", r"(\S+)")) # Pattern for word (all noblank characters), no () patternset.add_pattern(Pattern("jube_pat_nwrd", r"(?:\S+)")) # Pattern for blank space (variable length) patternset.add_pattern(Pattern("jube_pat_bl", r"(?:\s+)")) return patternset ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result.py0000664000174700017470000002251014603772010017327 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Resulttype definition""" from __future__ import (print_function, unicode_literals, division) import jube2.util.util import xml.etree.ElementTree as ET import re import jube2.log LOGGER = jube2.log.get_logger(__name__) class Result(object): """A generic result type""" class ResultData(object): """A gerneric result data type""" def __init__(self, name): self._name = name @property def name(self): """Return the result name""" return self._name def create_result(self, show=True, filename=None, **kwargs): """Create result output""" raise NotImplementedError("") def add_result_data(self, result_data): """Add additional result data""" raise NotImplementedError("") def __eq__(self, other): return self.name == other.name def __init__(self, name, res_filter=None): self._use = set() self._name = name self._res_filter = res_filter self._result_dir = None self._benchmark = None @property def name(self): """Return the result name""" return self._name @property def benchmark(self): """Return the benchmark""" return self._benchmark @property def result_dir(self): """Return the result_dir""" return self._result_dir @result_dir.setter def result_dir(self, result_dir): """Set the result_dir""" self._result_dir = result_dir @benchmark.setter def benchmark(self, benchmark): """Set the benchmark""" self._benchmark = benchmark def add_uses(self, use_names): """Add an addtional analyser name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) def create_result_data(self): """Create result representation""" raise NotImplementedError("") def _analyse_data(self): """Load analyse data out of given analysers""" for analyser_name in self._use: analyser = self._benchmark.analyser[analyser_name] analyse = analyser.analyse_result # Ignore empty analyse results if analyse is None: LOGGER.debug(("No data found for analyser \"{0}\" " "in benchmark run {1}. " "Run analyse step automatically.") .format(analyser_name, self._benchmark.id)) self._benchmark.analyse(show_info=False, specific_analyser_name=analyser_name) analyse = \ self._benchmark.analyser[analyser_name].analyse_result # Check if analyse is still empty if analyse is None: LOGGER.warning(("No data found for analyser \"{0}\" " "in benchmark run {1}.") .format(analyser_name, self._benchmark.id)) continue # Create workpackage chains wp_chains = list() all_wps = set() for ids in [analyse[stepname].keys() for stepname in analyse]: all_wps.update(set(map(int, ids))) # Find workpackages without children (or at least no childen in # the given analyser) last_wps = set() for id in all_wps: child_ids = set([wp.id for wp in self._benchmark. workpackage_by_id(id).children_future]) if not child_ids.intersection(all_wps): last_wps.add(id) while (len(last_wps) > 0): next_id = last_wps.pop() # Create new chain wp_chains.append(list()) # Add all parents to the chain for wp in self._benchmark.workpackage_by_id(next_id).\ parent_history: if wp.id not in wp_chains[-1]: wp_chains[-1].append(wp.id) # Add wp itself to the chain wp_chains[-1].append(next_id) # Create output datasets by combining analyse and parameter data for chain in wp_chains: analyse_dict = dict() for wp_id in chain: workpackage = self._benchmark.workpackage_by_id(wp_id) # add analyse data if (wp_id in all_wps): analyse_dict.update( analyse[workpackage.step.name][wp_id]) # add parameter parameter_dict = dict() for par in workpackage.parameterset: value = \ jube2.util.util.convert_type(par.parameter_type, par.value, stop=False) # add suffix to the parameter name if (par.name + "_" + workpackage.step.name not in parameter_dict): parameter_dict[par.name + "_" + workpackage.step.name] = value # parmater without suffix is used for the last WP in # the chain if wp_id == chain[-1]: parameter_dict[par.name] = value analyse_dict.update(parameter_dict) # Add jube additional information analyse_dict.update({ "jube_res_analyser": analyser_name, }) # If res_filter is set, only show matching result lines if self._res_filter is not None: res_filter = jube2.util.util.substitution( self._res_filter, analyse_dict) if not jube2.util.util.eval_bool(res_filter): continue yield analyse_dict def _load_units(self, pattern_names): """Load units""" units = dict() alt_pattern_names = list(pattern_names) for i, pattern_name in enumerate(alt_pattern_names): for option in ["first", "last", "min", "max", "avg", "sum", "std"]: matcher = re.match("^(.+)_{0}$".format(option), pattern_name) if matcher: alt_pattern_names[i] = matcher.group(1) for analyser_name in self._use: if analyser_name not in self._benchmark.analyser: raise RuntimeError( " not found".format(analyser_name)) patternset_names = \ self._benchmark.analyser[analyser_name].use.copy() for analyse_files in \ self._benchmark.analyser[analyser_name].analyser.values(): for analyse_file in analyse_files: for use in analyse_file.use: patternset_names.add(use) for patternset_name in patternset_names: patternset = self._benchmark.patternsets[patternset_name] for i, pattern_name in enumerate(pattern_names): alt_pattern_name = alt_pattern_names[i] if (pattern_name in patternset) or \ (alt_pattern_name in patternset): pattern = patternset[pattern_name] if pattern is None: pattern = patternset[alt_pattern_name] if (pattern.unit is not None) and (pattern.unit != ""): units[pattern_name] = pattern.unit for parameterset_name in self._benchmark.parametersets: parameterset = self._benchmark.parametersets[parameterset_name] for parameter in parameterset: if (parameter.unit is not None) and (parameter.unit != ""): units[parameter.name] = parameter.unit return units def etree_repr(self): """Return etree object representation""" result_etree = ET.Element("result") if self._result_dir is not None: result_etree.attrib["result_dir"] = self._result_dir for use in self._use: use_etree = ET.SubElement(result_etree, "use") use_etree.text = use return result_etree ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/jube2/result_types/0000775000174700017470000000000014603772011020202 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/__init__.py0000664000174700017470000000145214603772010022314 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2.result_types package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/database.py0000664000174700017470000001702414603772010022323 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Databasetype definition""" from __future__ import (print_function, unicode_literals, division) import sqlite3 import ast import os from jube2.result_types.genericresult import GenericResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log LOGGER = jube2.log.get_logger(__name__) class Database(GenericResult): """A database result""" class DatabaseData(GenericResult.KeyValuesData): """Database data""" def __init__(self, name_or_other, primekeys, db_file): if type(name_or_other) is GenericResult.KeyValuesData: self._name = name_or_other.name #self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: GenericResult.KeyValuesData.__init__(self, name_or_other) self._primekeys = primekeys self._db_file = None if db_file == "None" else db_file def create_result(self, show=True, filename=None, **kwargs): # Place for the magic # # show = If False do not show something on screen (result # only into file) # filename = name of standard output/datbase file # All keys: print([key.name for key in self._keys]) #col_names = [key.name for key in self._keys] # All data: print(self.data) keys = [k.name for k in self._data.keys()] # check if all primekeys are in keys if not set(self._primekeys).issubset(set(keys)): raise ValueError("primekeys are not included in !") # define database file if self._db_file is not None and filename is not None: file_handle = open(filename, "w") file_handle.write(self._db_file) file_handle.close() # create directory path to db file, if it does not exist file_path_ind = self._db_file.rfind('/') if file_path_ind != -1: # modify when Python2.7 support is dropped (potential race condition) if not os.path.exists(os.path.expanduser(self._db_file[:file_path_ind])): os.makedirs(os.path.expanduser( self._db_file[:file_path_ind])) db_file = os.path.expanduser(self._db_file) elif filename is not None: db_file = filename else: return None # create database and insert the data con = sqlite3.connect(db_file) cur = con.cursor() # create a string of keys and their data type to create the database table key_dtypes = {k.name: type(v[0]).__name__.replace( 'str', 'text') for (k, v) in self._data.items()} db_col_insert_types = str(key_dtypes).replace( '{', '(').replace('}', ')').replace("'", '').replace(':', '') if len(self._primekeys) > 0: db_col_insert_types = db_col_insert_types[:-1] + \ ", PRIMARY KEY ({}))".format(', '.join(map(repr, self._primekeys))) # create new table with a name of stored in variable self.name if it does not exists LOGGER.debug("CREATE TABLE IF NOT EXISTS {} {};".format( self.name, db_col_insert_types)) cur.execute("CREATE TABLE IF NOT EXISTS {} {};".format( self.name, db_col_insert_types)) # check for primary keys in database table cur.execute('PRAGMA TABLE_INFO({})'.format(self.name)) db_primary_keys = [i[1] for i in cur.fetchall() if i[5] != 0] if not set(self._primekeys) == set(db_primary_keys): raise ValueError("Modification of primary values is not supported. " + "Primary keys of table {} are {}".format(self.name, db_primary_keys)) # compare self._keys with columns in db and add new column in the database if it does not exist cur.execute("SELECT * FROM {}".format(self.name)) db_col_names = [tup[0] for tup in cur.description] # delete columns, which were removed as keys in this execution diff_col_list = list(set(db_col_names).difference(keys)) if len(diff_col_list) != 0: for col in diff_col_list: LOGGER.debug( "ALTER TABLE {} DROP COLUMN {}".format(self.name, col)) cur.execute( "ALTER TABLE {} DROP COLUMN {}".format(self.name, col)) # add columns, which were added as keys in this execution diff_col_list = list(set(keys).difference(db_col_names)) if len(diff_col_list) != 0: for col in diff_col_list: LOGGER.debug("ALTER TABLE {} ADD COLUMN {} {}".format( self.name, col, type(col).__name__.replace('str', 'text'))) cur.execute("ALTER TABLE {} ADD COLUMN {} {}".format( self.name, col, type(col).__name__.replace('str', 'text'))) # insert or replace self.data in database replace_query = "REPLACE INTO {} {} VALUES (".format( self.name, tuple(keys)) + "{}".format('?,'*len(keys))[:-1] + ");" LOGGER.debug(replace_query) cur.executemany( replace_query, [d for d in list(zip(*self._data.values()))]) con.commit() con.close() # Print database location to screen and result.log LOGGER.info("Database location of id {}: {}".format( self._benchmark_ids[0], db_file)) def __init__(self, name, res_filter=None, primekeys=None, db_file=None): GenericResult.__init__(self, name, res_filter) self._primekeys = primekeys self._db_file = db_file def create_result_data(self, style=None, select=None, exclude=None): """Create result data""" result_data = GenericResult.create_result_data(self, select, exclude) return Database.DatabaseData(result_data, self._primekeys, self._db_file) def etree_repr(self): """Return etree object representation""" result_etree = Result.etree_repr(self) database_etree = ET.SubElement(result_etree, "database") database_etree.attrib["name"] = self._name if self._res_filter is not None: database_etree.attrib["filter"] = self._res_filter for key in self._keys: database_etree.append(key.etree_repr()) database_etree.attrib["primekeys"] = str(self._primekeys) database_etree.attrib["file"] = str(self._db_file) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/genericresult.py0000664000174700017470000002220414603772010023426 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """GernicResultType definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result import Result import jube2.log import xml.etree.ElementTree as ET import operator import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class GenericResult(Result): """A generic result type""" class KeyValuesData(Result.ResultData): """Generic key value data""" def __init__(self, other_or_name): if type(other_or_name) is str: Result.ResultData.__init__(self, other_or_name) elif type(other_or_name) is Result.ResultData: self._name = other_or_name.name self._data = dict() self._benchmark_ids = list() @property def keys(self): """Return keys""" return self._data.keys() @property def data(self): """Return data""" return self._data @property def data_dict(self): """Return unordered dictionary representation of data""" return self._data @property def benchmark_ids(self): """Return benchmark ids""" return self._benchmark_ids def add_key_value_data(self, data, benchmark_ids): """Add a list of additional rows to current result data""" # Add new keys to for old rows for key in data.keys(): if key not in self._data.keys(): if len(self._benchmark_ids) > 0: self._data[key] = [None] * len(self._benchmark_ids) else: self._data[key] = list() number_of_new_values = len(list(data.values())[0]) # Add new rows for key in self._data.keys(): if key in data.keys(): self._data[key] += data[key] else: self._data[key] += [None] * number_of_new_values if type(benchmark_ids) is int: self._benchmark_ids += [benchmark_ids] * number_of_new_values if type(benchmark_ids) is list: self._benchmark_ids += benchmark_ids def add_result_data(self, result_data): """Add additional result data""" if self.name != result_data.name: raise RuntimeError("Cannot combine to different result sets.") self.add_key_value_data(result_data.data, result_data.benchmark_ids) def create_result(self, show=True, filename=None, **kwargs): """Create result representation""" raise NotImplementedError("") class DataKey(object): """Class represents one data key """ def __init__(self, name, title=None, unit=None): self._name = name self._title = title self._unit = unit @property def title(self): """Key title""" return self._title @property def name(self): """Key name""" return self._name @property def unit(self): """Key data unit""" return self._unit @unit.setter def unit(self, unit): """Set key data unit""" self._unit = unit @property def resulting_name(self): """Column name based on name, title and unit""" if self._title is not None: name = self._title else: name = self._name if self._unit is not None: name += "[{0}]".format(self._unit) return name def etree_repr(self): """Return etree object representation""" key_etree = ET.Element("key") key_etree.text = self._name if self._title is not None: key_etree.attrib["title"] = self._title return key_etree def __eq__(self, other): return self.resulting_name == other.resulting_name def __hash__(self): return hash(self.resulting_name) def __init__(self, name, res_filter=None): Result.__init__(self, name, res_filter) self._keys = list() def add_key(self, name, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(GenericResult.DataKey(name, title, unit)) def create_result_data(self, select, exclude): """Create result data""" result_data = GenericResult.KeyValuesData(self._name) if exclude is None: exclude = [] if select is None: select = [key.name for key in self._keys] else: # Check whether the same column name appears in select and exclude if set(select) & set(exclude): LOGGER.error("Error when checking the select and exclude names: " "A pattern or parameter name occurs in both select " "and exclude") exit() # Read pattern/parameter units if available units = self._load_units([key.name for key in self._keys]) for key in self._keys: if key.name in units: key.unit = units[key.name] # Check for correctness of exclude and select names key_names = [key.name for key in self._keys] # Help lists for multiple columns unique_select = [] multiple_select = [] for select_name in select: # Check if given names exist in keys if select_name not in key_names: LOGGER.warning("The result database does not contain a pattern " "or parameter with the name '{0}'. This " "name will be ignored for selection." .format(select_name)) # Check whether the given name occurs only once if select_name not in unique_select: unique_select.append(select_name) elif select_name not in multiple_select: multiple_select.append(select_name) LOGGER.warning("The pattern or parameter name {} occurs more " "than once. These additional occurrences are " "ignored for selection.".format(select_name)) # Help lists for multiple columns unique_exclude = [] multiple_exclude = [] for exclude_name in exclude: # Check if given names exist in keys if exclude_name not in key_names: LOGGER.warning("The result database does not contain a pattern " "or parameter with the name '{0}'. This " "name will be ignored for exclusion." .format(exclude_name)) # Check whether the given name occurs only once if exclude_name not in unique_exclude: unique_exclude.append(exclude_name) elif exclude_name not in multiple_exclude: multiple_exclude.append(exclude_name) LOGGER.warning("The pattern or parameter name {} occurs more " "than once. These additional occurrences are " "ignored for exclusion.".format(exclude_name)) # Select and exclude table columns self._keys = [key for key in self._keys if key.name in select and \ key.name not in exclude] # Create result data data = dict() for dataset in self._analyse_data(): new_data = dict() cnt = 0 for key in self._keys: if key.name in dataset: # Cnt number of final entries to avoid complete empty # result entries cnt += 1 new_data[key] = dataset[key.name] else: new_data[key] = None if cnt > 0: for key in new_data: if key not in data: data[key] = list() data[key].append(new_data[key]) # Add data to the result set result_data.add_key_value_data(data, self._benchmark.id) return result_data ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/keyvaluesresult.py0000664000174700017470000003022214603772010024021 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """KeyValuesResulttype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result import Result import jube2.log import xml.etree.ElementTree as ET import operator import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class KeyValuesResult(Result): """A generic key value result type""" class KeyValuesData(Result.ResultData): """Key value data""" def __init__(self, other_or_name): if type(other_or_name) is str: Result.ResultData.__init__(self, other_or_name) elif type(other_or_name) is Result.ResultData: self._name = other_or_name.name self._data = list() self._keys = list() self._benchmark_ids = list() @property def keys(self): """Return keys""" return self._keys @property def data(self): """Return table data""" return self._data @property def data_dict(self): """Return unordered dictionary representation of data""" result_dict = dict() for i, key in enumerate(self._keys): result_dict[key] = list() for data in self._data: result_dict[key].append(data[i]) return result_dict @property def benchmark_ids(self): """Return benchmark ids""" return self._benchmark_ids def add_key_value_data(self, keys, data, benchmark_ids): """Add a list of additional rows to current result data""" order = list() last_index = len(self._keys) # Find matching rows for key in keys: if key in self._keys: index = self._keys.index(key) # Check weather key occurs multiple times while index in order: try: index = self._keys.index(key, index + 1) except ValueError: index = len(self._keys) self._keys.append(key) else: index = len(self._keys) self._keys.append(key) order.append(index) # Fill up existing rows if last_index != len(self._keys): for row in self._data: row += ["" for key in self._keys[last_index:]] # Add new rows for row in data: new_row = ["" for key in self._keys] for i, index in enumerate(order): new_row[index] = row[i] self._data.append(new_row) if type(benchmark_ids) is int: self._benchmark_ids.append(benchmark_ids) if type(benchmark_ids) is list: self._benchmark_ids += benchmark_ids def add_id_information(self, reverse=False): """Add additional id key to table data.""" id_key = KeyValuesResult.DataKey("id") if id_key not in self._keys: # Add key at the beginning of keys list self._keys.insert(0, id_key) for i, data in enumerate(self._data): data.insert(0, self._benchmark_ids[i]) # Sort data by using new id key (stable sort) self._data.sort(key=operator.itemgetter(0), reverse=reverse) for i, data in enumerate(self._data): self._data[i][0] = str(data[0]) def add_result_data(self, result_data): """Add additional result data""" if self.name != result_data.name: raise RuntimeError("Cannot combine to different result sets.") self.add_key_value_data(result_data.keys, result_data.data, result_data.benchmark_ids) def create_result(self, show=True, filename=None, **kwargs): """Create result representation""" raise NotImplementedError("") class DataKey(object): """Class represents one data key """ def __init__(self, name, title=None, format_string=None, unit=None): self._name = name self._title = title self._format_string = format_string self._unit = unit @property def title(self): """Key title""" return self._title @property def name(self): """Key name""" return self._name @property def format(self): """Key data format""" return self._format_string @property def unit(self): """Key data unit""" return self._unit @unit.setter def unit(self, unit): """Set key data unit""" self._unit = unit @property def resulting_name(self): """Column name based on name, title and unit""" if self._title is not None: name = self._title else: name = self._name if self._unit is not None: name += "[{0}]".format(self._unit) return name def etree_repr(self): """Return etree object representation""" key_etree = ET.Element("key") key_etree.text = self._name if self._format_string is not None: key_etree.attrib["format"] = self._format_string if self._title is not None: key_etree.attrib["title"] = self._title return key_etree def __eq__(self, other): return self.resulting_name == other.resulting_name def __hash__(self): return hash(self.resulting_name) def __init__(self, name, sort_names=None, res_filter=None): Result.__init__(self, name, res_filter) self._keys = list() if sort_names is None: self._sort_names = list() else: self._sort_names = sort_names def add_key(self, name, format_string=None, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(KeyValuesResult.DataKey(name, title, format_string, unit)) def create_result_data(self, select=None, exclude=None): """Create result data""" result_data = KeyValuesResult.KeyValuesData(self._name) if exclude is None: exclude = [] if select is None: select = [key.name for key in self._keys] else: # Check whether the same column name appears in select and exclude if set(select) & set(exclude): LOGGER.error("Error when checking the select and exclude names: " "A pattern or parameter name occurs in both select " "and exclude") exit() # Read pattern/parameter units if available units = self._load_units([key.name for key in self._keys]) for key in self._keys: if key.name in units: key.unit = units[key.name] sort_data = list() for dataset in self._analyse_data(): # Add additional data if needed for sort_name in self._sort_names: if sort_name not in dataset: dataset[sort_name] = None sort_data.append(dataset) # Sort the resultset if len(self._sort_names) > 0: LOGGER.debug("sort using: {0}".format(",".join(self._sort_names))) # Use CompType for sorting to allow comparison of None values sort_data = \ sorted(sort_data, key=lambda x: [jube2.util.util.CompType(x[sort_name]) for sort_name in self._sort_names]) # Check for correctness of exclude and select names key_names = [key.name for key in self._keys] # Help lists for multiple columns unique_select = [] multiple_select = [] for select_name in select: # Check if given names exist in keys if select_name not in key_names: LOGGER.warning("The result table does not contain a pattern " "or parameter with the name '{0}'. This " "name will be ignored for selection." .format(select_name)) # Check whether the given name occurs only once if select_name not in unique_select: unique_select.append(select_name) elif select_name not in multiple_select: multiple_select.append(select_name) LOGGER.warning("The pattern or parameter name {} occurs more " "than once. These additional occurrences are " "ignored for selection.".format(select_name)) # Help lists for multiple columns unique_exclude = [] multiple_exclude = [] for exclude_name in exclude: # Check if given names exist in keys if exclude_name not in key_names: LOGGER.warning("The result table does not contain a pattern " "or parameter with the name '{0}'. This " "name will be ignored for exclusion." .format(exclude_name)) # Check whether the given name occurs only once if exclude_name not in unique_exclude: unique_exclude.append(exclude_name) elif exclude_name not in multiple_exclude: multiple_exclude.append(exclude_name) LOGGER.warning("The pattern or parameter name {} occurs more " "than once. These additional occurrences are " "ignored for exclusion.".format(exclude_name)) # Select and exclude table columns self._keys = [key for key in self._keys if key.name in select and \ key.name not in exclude] # Create table data table_data = list() for dataset in sort_data: row = list() cnt = 0 for key in self._keys: if key.name in dataset: # Cnt number of final entries to avoid complete empty # result entries cnt += 1 # Set null value if dataset[key.name] is None: value = "" else: # Format data values to create string representation if key.format is not None: value = jube2.util.output.format_value( key.format, dataset[key.name]) else: value = str(dataset[key.name]) row.append(value) else: row.append(None) if cnt > 0: table_data.append(row) # Add data to toe result set result_data.add_key_value_data(self._keys, table_data, self._benchmark.id) return result_data ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/syslog.py0000664000174700017470000001362214603772010022077 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Syslogtype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result_types.keyvaluesresult import KeyValuesResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log import jube2.conf import logging.handlers LOGGER = jube2.log.get_logger(__name__) class SysloggedResult(KeyValuesResult): """A result that gets sent to syslog.""" class SyslogData(KeyValuesResult.KeyValuesData): """Syslog data""" def __init__(self, name_or_other, syslog_address=None, syslog_host=None, syslog_port=None, syslog_fmt_string=None): if type(name_or_other) is KeyValuesResult.KeyValuesData: self._name = name_or_other.name self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: KeyValuesResult.KeyValuesData.__init__(self, name_or_other) self._syslog_address = syslog_address self._syslog_host = syslog_host self._syslog_port = syslog_port self._syslog_fmt_string = syslog_fmt_string def create_result(self, show=True, filename=None, **kwargs): """Create result output""" # If there are multiple benchmarks, add benchmark id information if len(set(self._benchmark_ids)) > 1: self.add_id_information(reverse=kwargs.get("reverse", False)) if self._syslog_address is not None: address = self._syslog_address else: address = (self._syslog_host, self._syslog_port) handler = logging.handlers.SysLogHandler( address=address, facility=logging.handlers.SysLogHandler.LOG_USER ) handler.setFormatter(logging.Formatter( fmt=self._syslog_fmt_string)) # get logger log = logging.getLogger("jube") log.setLevel(logging.INFO) log.addHandler(handler) # create log output for dataset in self.data: entry = list() for i, key in enumerate(self.keys): entry.append("{0}={1}".format(key.name, dataset[i])) # Log result if show: if not jube2.conf.DEBUG_MODE: log.info(" ".join(entry)) LOGGER.debug("Logged: {0}\n".format(" ".join(entry))) # remove handler to avoid double logging log.removeHandler(handler) def __init__(self, name, syslog_address=None, syslog_host=None, syslog_port=None, syslog_fmt_string=None, sort_names=None, res_filter=None): KeyValuesResult.__init__(self, name, sort_names, res_filter) if (syslog_address is None) and (syslog_host is None) and \ (syslog_port is None): raise IOError("Neither a syslog address nor a hostname port " + "combination specified.") if (syslog_host is not None) and (syslog_address is not None): raise IOError("Please specify a syslog address or a hostname, " + "not both at the same time.") if (syslog_host is not None) and (syslog_port is None): self._syslog_port = 514 self._syslog_address = syslog_address self._syslog_host = syslog_host self._syslog_port = syslog_port if syslog_fmt_string is None: self._syslog_fmt_string = jube2.conf.SYSLOG_FMT_STRING else: self._syslog_fmt_string = syslog_fmt_string def create_result_data(self, style=None, select=None, exclude=None): """Create result data""" result_data = KeyValuesResult.create_result_data(self, select, exclude) return SysloggedResult.SyslogData(result_data, self._syslog_address, self._syslog_host, self._syslog_port, self._syslog_fmt_string) def etree_repr(self): """Return etree object representation""" result_etree = Result.etree_repr(self) syslog_etree = ET.SubElement(result_etree, "syslog") syslog_etree.attrib["name"] = self._name if self._syslog_address is not None: syslog_etree.attrib["address"] = self._syslog_address if self._syslog_host is not None: syslog_etree.attrib["host"] = self._syslog_host if self._syslog_port is not None: syslog_etree.attrib["port"] = self._syslog_port if self._syslog_fmt_string is not None: syslog_etree.attrib["format"] = self._syslog_fmt_string if self._res_filter is not None: syslog_etree.attrib["filter"] = self._res_filter if len(self._sort_names) > 0: syslog_etree.attrib["sort"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._sort_names) for key in self._keys: syslog_etree.append(key.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/result_types/table.py0000664000174700017470000001577414603772010021660 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Tabletype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result_types.keyvaluesresult import KeyValuesResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class Table(KeyValuesResult): """A ascii based result table""" class TableData(KeyValuesResult.KeyValuesData): """Table data""" def __init__(self, name_or_other, style, separator, transpose): if type(name_or_other) is KeyValuesResult.KeyValuesData: self._name = name_or_other.name self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: KeyValuesResult.KeyValuesData.__init__(self, name_or_other) self._style = style self._separator = separator # Ignore separator if pretty style is used if self._style == "pretty": self._separator = None elif self._separator is None: self._separator = jube2.conf.DEFAULT_SEPARATOR self._transpose = transpose @property def _columns(self): """Get columns""" return self._keys @property def style(self): """Get style""" return self._style @style.setter def style(self, style): """Set style""" self._style = style @property def separator(self): """Get separator""" return self._separator @separator.setter def separator(self, separator): """Set separator""" self._separator = separator def __str__(self): colw = list() for column in self._columns: if type(column) is Table.Column: if column.colw is None: colw.append(0) else: colw.append(column.colw) else: colw.append(0) data = list() data.append([column.resulting_name for column in self._columns]) data += self._data data = [['' if c is None else c for c in r] for r in data] if self._style == "pretty": output = "{0}:\n".format(self.name) else: output = "" output += jube2.util.output.text_table( data, use_header_line=True, auto_linebreak=False, colw=colw, indent=0, style=self._style, separator=self._separator, transpose=self._transpose) return output def create_result(self, show=True, filename=None, **kwargs): """Create result output""" # If there are multiple benchmarks, add benchmark id information if len(set(self._benchmark_ids)) > 1: self.add_id_information(reverse=kwargs.get("reverse", False)) result_str = str(self) # Print result to screen if show: LOGGER.info(result_str) LOGGER.info("\n") else: LOGGER.debug(result_str) LOGGER.debug("\n") # Print result to file if filename is not None: file_handle = open(filename, "w") file_handle.write(result_str) file_handle.close() class Column(KeyValuesResult.DataKey): """Class represents one table column""" def __init__(self, name, title=None, colw=None, format_string=None, unit=None): KeyValuesResult.DataKey.__init__(self, name, title, format_string, unit) self._colw = colw @property def colw(self): """Column width""" return self._colw def etree_repr(self): """Return etree object representation""" column_etree = KeyValuesResult.DataKey.etree_repr(self) column_etree.tag = "column" if self._colw is not None: column_etree.attrib["colw"] = str(self._colw) return column_etree def __init__(self, name, style="csv", separator=jube2.conf.DEFAULT_SEPARATOR, sort_names=None, transpose=False, res_filter=None): KeyValuesResult.__init__(self, name, sort_names, res_filter) self._style = style self._separator = separator self._transpose = transpose def add_column(self, name, colw=None, format_string=None, title=None): """Add an additional column to the dataset""" self._keys.append(Table.Column(name, title, colw, format_string)) def add_key(self, name, format_string=None, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(Table.Column(name, title, None, format_string, unit)) def create_result_data(self, style, select=None, exclude=None): """Create result data""" result_data = KeyValuesResult.create_result_data(self, select, exclude) return Table.TableData(result_data, style if style is not None else self._style, self._separator, self._transpose) def etree_repr(self): """Return etree object representation""" result_etree = Result.etree_repr(self) table_etree = ET.SubElement(result_etree, "table") table_etree.attrib["name"] = self._name table_etree.attrib["style"] = self._style if self._separator is not None: table_etree.attrib["separator"] = self._separator if self._res_filter is not None: table_etree.attrib["filter"] = self._res_filter table_etree.attrib["transpose"] = str(self._transpose) if len(self._sort_names) > 0: table_etree.attrib["sort"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._sort_names) for column in self._keys: table_etree.append(column.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/step.py0000664000174700017470000010173214603772010016770 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Step contains the commands for steps""" from __future__ import (print_function, unicode_literals, division) import subprocess import os import re import time import xml.etree.ElementTree as ET import jube2.util.util import jube2.conf import jube2.log import jube2.parameter LOGGER = jube2.log.get_logger(__name__) class Step(object): """A Step represent one execution step. It contains a list of Do-operations and multiple parametersets, substitutionsets and filesets. A Step is a template for Workpackages. """ def __init__(self, name, depend, iterations=1, alt_work_dir=None, shared_name=None, export=False, max_wps="0", active="true", suffix="", cycles=1, procs=1, do_log_file=None): self._name = name self._use = list() self._operations = list() self._iterations = iterations self._depend = depend self._alt_work_dir = alt_work_dir self._shared_name = shared_name self._export = export self._max_wps = max_wps self._active = active self._suffix = suffix self._cycles = cycles self._procs = procs self._do_log_file = do_log_file def etree_repr(self): """Return etree object representation""" step_etree = ET.Element("step") step_etree.attrib["name"] = self._name if len(self._depend) > 0: step_etree.attrib["depend"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._depend) if self._alt_work_dir is not None: step_etree.attrib["work_dir"] = self._alt_work_dir if self._shared_name is not None: step_etree.attrib["shared"] = self._shared_name if self._active != "true": step_etree.attrib["active"] = self._active if self._suffix != "": step_etree.attrib["suffix"] = self._suffix if self._export: step_etree.attrib["export"] = "true" if self._max_wps != "0": step_etree.attrib["max_async"] = self._max_wps if self._iterations > 1: step_etree.attrib["iterations"] = str(self._iterations) if self._cycles > 1: step_etree.attrib["cycles"] = str(self._cycles) if self._procs != 1: step_etree.attrib["procs"] = str(self._procs) if self._do_log_file != None: step_etree.attrib["do_log_file"] = str(self._do_log_file) for use in self._use: use_etree = ET.SubElement(step_etree, "use") use_etree.text = jube2.conf.DEFAULT_SEPARATOR.join(use) for operation in self._operations: step_etree.append(operation.etree_repr()) return step_etree def __repr__(self): return "{0}".format(vars(self)) def add_operation(self, operation): """Add operation""" self._operations.append(operation) def add_uses(self, use_names): """Add use""" for use_name in use_names: if any([use_name in use_list for use_list in self._use]): raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.append(use_names) @property def name(self): """Return step name""" return self._name @property def active(self): """Return active state""" return self._active @property def export(self): """Return export behaviour""" return self._export @property def iterations(self): """Return iterations""" return self._iterations @property def cycles(self): """Return number of cycles""" return self._cycles @property def procs(self): """Return number of procs""" return self._procs @property def shared_link_name(self): """Return shared link name""" return self._shared_name @property def max_wps(self): """Return maximum number of simultaneous workpackages""" return self._max_wps @property def do_log_file(self): """Return do log file name""" return self._do_log_file def get_used_sets(self, available_sets, parameter_dict=None): """Get list of all used sets, which can be found in available_sets""" set_names = list() if parameter_dict is None: parameter_dict = dict() for use in self._use: for name in use: name = jube2.util.util.substitution(name, parameter_dict) if (name in available_sets) and (name not in set_names): set_names.append(name) return set_names def shared_folder_path(self, benchdir, parameter_dict=None): """Return shared folder name""" if self._shared_name is not None: if parameter_dict is not None: shared_name = jube2.util.util.substitution(self._shared_name, parameter_dict) else: shared_name = self._shared_name return os.path.join(benchdir, "{0}_{1}".format(self._name, shared_name)) else: return "" def get_jube_parameterset(self): """Return parameterset which contains step related information""" parameterset = jube2.parameter.Parameterset() # step name parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_name", self._name, update_mode=jube2.parameter.JUBE_MODE)) # iterations parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_iterations", str(self._iterations), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # cycles parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_cycles", str(self._cycles), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # default worpackage cycle, will be overwritten by specific worpackage # cycle parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_cycle", "0", parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_workpackages(self, benchmark, global_parameterset, local_parameterset=None, used_sets=None, iteration_base=0, parents=None, incompatible_parameters=None): """Create workpackages for current step using given benchmark context""" if used_sets is None: used_sets = set() update_parameters = jube2.parameter.Parameterset() if local_parameterset is None: local_parameterset = jube2.parameter.Parameterset() global_parameterset.add_parameterset( benchmark.get_jube_parameterset()) global_parameterset.add_parameterset(self.get_jube_parameterset()) update_parameters.add_parameterset( global_parameterset.get_updatable_parameter( jube2.parameter.STEP_MODE)) for parameter in update_parameters: incompatible_parameters.discard(parameter.name) if parents is None: parents = list() new_workpackages = list() # Create parameter dictionary for substitution parameter_dict = \ dict([[par.name, par.value] for par in global_parameterset.constant_parameter_dict.values()]) # Filter for parametersets in uses parameterset_names = \ set(self.get_used_sets(benchmark.parametersets, parameter_dict)) new_sets_found = len(parameterset_names.difference(used_sets)) > 0 if new_sets_found: parameterset_names = parameterset_names.difference(used_sets) used_sets = used_sets.union(parameterset_names) for parameterset_name in parameterset_names: # The parametersets in a single step must be compatible if not local_parameterset.is_compatible( benchmark.parametersets[parameterset_name]): incompatible_names = \ local_parameterset.get_incompatible_parameter( benchmark.parametersets[parameterset_name]) raise ValueError(("Cannot use parameterset '{0}' in " + "step '{1}'.\nParameter '{2}' is/are " + "already defined by a different " + "parameterset.") .format(parameterset_name, self.name, ",".join(incompatible_names))) local_parameterset.add_parameterset( benchmark.parametersets[parameterset_name]) # Combine local and history parameterset if local_parameterset.is_compatible( global_parameterset, update_mode=jube2.parameter.USE_MODE): update_parameters.add_parameterset( local_parameterset.get_updatable_parameter( jube2.parameter.USE_MODE)) for parameter in update_parameters: incompatible_parameters.discard(parameter.name) global_parameterset = \ local_parameterset.copy().add_parameterset( global_parameterset) else: incompatible_names = \ local_parameterset.get_incompatible_parameter( global_parameterset, update_mode=jube2.parameter.USE_MODE) LOGGER.debug("Incompatible parameterset combination found " + "between current and parent steps. \nParameter " + "'{0}' is/are already defined different.".format( ",".join(incompatible_names))) return new_workpackages # update parameters global_parameterset.update_parameterset(update_parameters) # Set tag-mode evaluation helper function to allow access to tag list # during paramter evaluation for parameter in global_parameterset.all_parameters: if parameter.mode == "tag": parameter.eval_helper = \ lambda tag: tag if tag in benchmark.tags else "" # Expand templates parametersets = [global_parameterset] change = True while change: change = False new_parametersets = list() for parameterset in parametersets: parameterset.parameter_substitution() # Maybe new templates were created if parameterset.has_templates: LOGGER.debug("Expand parameter templates:\n{0}".format( "\n".join(" \"{0}\": {1}".format(i, j.value) for i, j in parameterset. template_parameter_dict.items()))) new_parametersets += \ [new_parameterset for new_parameterset in parameterset.expand_templates()] change = True else: new_parametersets += [parameterset] parametersets = new_parametersets # Create workpackages for parameterset in parametersets: workpackage_parameterset = local_parameterset.copy() workpackage_parameterset.update_parameterset(parameterset) if new_sets_found: new_workpackages += \ self.create_workpackages(benchmark, parameterset, workpackage_parameterset, used_sets, iteration_base, parents, incompatible_parameters.copy()) else: # Check if all incompatible_parameters were updated if len(incompatible_parameters) > 0: return new_workpackages # Create new workpackage created_workpackages = list() for iteration in range(self.iterations): workpackage = jube2.workpackage.Workpackage( benchmark=benchmark, step=self, parameterset=parameterset.copy(), local_parameter_names=[ par.name for par in workpackage_parameterset], iteration=iteration_base * self.iterations + iteration, cycle=0) # --- Link parent workpackages --- for parent in parents: workpackage.add_parent(parent) # --- Add workpackage JUBE parameterset --- workpackage.parameterset.add_parameterset( workpackage.get_jube_parameterset()) # --- Final parameter substitution --- workpackage.parameterset.parameter_substitution( final_sub=True) # --- Check parameter type --- for parameter in workpackage.parameterset: if not parameter.is_template: jube2.util.util.convert_type( parameter.parameter_type, parameter.value) # --- Enable workpackage dir cache --- workpackage.allow_workpackage_dir_caching() if workpackage.active: created_workpackages.append(workpackage) else: jube2.workpackage.Workpackage.\ reduce_workpackage_id_counter() for workpackage in created_workpackages: workpackage.iteration_siblings.update( set(created_workpackages)) new_workpackages += created_workpackages return new_workpackages @property def alt_work_dir(self): """Return alternativ work directory""" return self._alt_work_dir @property def use(self): """Return parameters and substitutions""" return self._use @property def suffix(self): """Return directory suffix""" return self._suffix @property def operations(self): """Return operations""" return self._operations @property def depend(self): """Return dependencies""" return self._depend def get_depend_history(self, benchmark): """Creates a set of all dependent steps in history for given benchmark""" depend_history = set() for step_name in self._depend: if step_name not in depend_history: depend_history.add(step_name) depend_history.update( benchmark.steps[step_name].get_depend_history(benchmark)) return depend_history class Operation(object): """The Operation-class represents a single instruction, which will be executed in a shell environment. """ def __init__(self, do, async_filename=None, stdout_filename=None, stderr_filename=None, active="true", shared=False, work_dir=None, break_filename=None, error_filename=None): self._do = do self._error_filename = error_filename self._async_filename = async_filename self._break_filename = break_filename self._stdout_filename = stdout_filename self._stderr_filename = stderr_filename self._active = active self._shared = shared self._work_dir = work_dir @property def stdout_filename(self): """Get stdout filename""" return self._stdout_filename @property def stderr_filename(self): """Get stderr filename""" return self._stderr_filename @property def error_filename(self): """Get error filename""" return self._error_filename @property def async_filename(self): """Get async filename""" return self._async_filename @property def shared(self): """Shared operation?""" return self._shared def active(self, parameter_dict): """Return active status of the current operation depending on the given parameter_dict""" active_str = jube2.util.util.substitution(self._active, parameter_dict) return jube2.util.util.eval_bool(active_str) def execute(self, parameter_dict, work_dir, only_check_pending=False, environment=None, pid=None, dolog=None): """Execute the operation. work_dir must be set to the given context path. The parameter_dict used for inline substitution. If only_check_pending is set to True, the operation will not be executed, only the async_file will be checked. Return operation status: True => operation finished False => operation pending """ if not self.active(parameter_dict): return True if environment is not None: env = environment else: env = os.environ if not only_check_pending: # Inline substitution do = jube2.util.util.substitution(self._do, parameter_dict) # Remove leading and trailing ; because otherwise ;; will cause # trouble when adding ; env do = do.strip(";") if (not jube2.conf.DEBUG_MODE) and (do.strip() != ""): # Change stdout if self._stdout_filename is not None: stdout_filename = jube2.util.util.substitution( self._stdout_filename, parameter_dict) stdout_filename = \ os.path.expandvars(os.path.expanduser(stdout_filename)) else: stdout_filename = "stdout" stdout_path = os.path.join(work_dir, stdout_filename) stdout = open(stdout_path, "a") # Change stderr if self._stderr_filename is not None: stderr_filename = jube2.util.util.substitution( self._stderr_filename, parameter_dict) stderr_filename = \ os.path.expandvars(os.path.expanduser(stderr_filename)) else: stderr_filename = "stderr" stderr_path = os.path.join(work_dir, stderr_filename) stderr = open(stderr_path, "a") # Use operation specific work directory if self._work_dir is not None and len(self._work_dir) > 0: new_work_dir = jube2.util.util.substitution( self._work_dir, parameter_dict) new_work_dir = os.path.expandvars(os.path.expanduser(new_work_dir)) work_dir = os.path.join(work_dir, new_work_dir) if re.search(jube2.parameter.Parameter.parameter_regex, work_dir): raise IOError(("Given work directory {0} contains a unknown " + "JUBE or environment variable.").format( work_dir)) # Create directory if it does not exist if not jube2.conf.DEBUG_MODE and not os.path.exists(work_dir): try: os.makedirs(work_dir) except FileExistsError: pass if not only_check_pending: if pid is not None: env_file_name = jube2.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube2.conf.ENVIRONMENT_INFO abs_info_file_path = \ os.path.abspath(os.path.join(work_dir, env_file_name)) # Select unix shell shell = jube2.conf.STANDARD_SHELL if "JUBE_EXEC_SHELL" in os.environ: alt_shell = os.environ["JUBE_EXEC_SHELL"].strip() if len(alt_shell) > 0: shell = alt_shell # Execute "do" LOGGER.debug(">>> {0}".format(do)) if (not jube2.conf.DEBUG_MODE) and (do != ""): LOGGER.debug(" stdout: {0}".format( os.path.abspath(stdout_path))) LOGGER.debug(" stderr: {0}".format( os.path.abspath(stderr_path))) try: if jube2.conf.VERBOSE_LEVEL > 1: stdout_handle = subprocess.PIPE else: stdout_handle = stdout if dolog != None: dolog.store_do(do=do, shell=shell, work_dir=os.path.abspath( work_dir), parameter_dict=parameter_dict, shared=self.shared) sub = subprocess.Popen( [shell, "-c", "{0} && env > \"{1}\"".format(do, abs_info_file_path)], cwd=work_dir, stdout=stdout_handle, stderr=stderr, shell=False, env=env) except OSError: stdout.close() stderr.close() raise RuntimeError(("Error (returncode <> 0) while " + "running \"{0}\" in " + "directory \"{1}\"") .format(do, os.path.abspath(work_dir))) # stdout verbose output if jube2.conf.VERBOSE_LEVEL > 1: while True: read_out = sub.stdout.read( jube2.conf.VERBOSE_STDOUT_READ_CHUNK_SIZE) if (not read_out): break else: try: print(read_out.decode(errors="ignore"), end="") except TypeError: print(read_out.decode("utf-8", "ignore"), end="") try: stdout.write(read_out) except TypeError: try: stdout.write(read_out.decode( errors="ignore")) except TypeError: stdout.write(read_out.decode("utf-8", "ignore")) time.sleep(jube2.conf.VERBOSE_STDOUT_POLL_SLEEP) sub.communicate() returncode = sub.wait() # Close filehandles stdout.close() stderr.close() env = Operation.read_process_environment(work_dir, pid=pid) # Read and store new environment if (environment is not None) and (returncode == 0): environment.clear() environment.update(env) if returncode != 0: if os.path.isfile(stderr_path): stderr = open(stderr_path, "r") stderr_msg = stderr.readlines() stderr.close() else: stderr_msg = "" try: raise RuntimeError( ("Error (returncode <> 0) while running \"{0}\" " + "in directory \"{1}\"\nMessage in \"{2}\":" + "{3}\n{4}").format( do, os.path.abspath(work_dir), os.path.abspath(stderr_path), "\n..." if len(stderr_msg) > jube2.conf.ERROR_MSG_LINES else "", "\n".join(stderr_msg[ -jube2.conf.ERROR_MSG_LINES:]))) except UnicodeDecodeError: raise RuntimeError( ("Error (returncode <> 0) while running \"{0}\" " + "in directory \"{1}\"").format( do, os.path.abspath(work_dir))) continue_op = True continue_cycle = True # Check if further execution was skipped if self._break_filename is not None: break_filename = jube2.util.util.substitution( self._break_filename, parameter_dict) break_filename = \ os.path.expandvars(os.path.expanduser(break_filename)) if os.path.exists(os.path.join(work_dir, break_filename)): LOGGER.debug(("\"{0}\" was found, workpackage execution and " " further loop continuation was stopped.") .format(break_filename)) continue_cycle = False # Waiting to continue if self._async_filename is not None: async_filename = jube2.util.util.substitution( self._async_filename, parameter_dict) async_filename = \ os.path.expandvars(os.path.expanduser(async_filename)) if not os.path.exists(os.path.join(work_dir, async_filename)): LOGGER.debug("Waiting for file \"{0}\" ..." .format(async_filename)) if jube2.conf.DEBUG_MODE: LOGGER.debug(" skip waiting") else: continue_op = False # Search for error file if self._error_filename is not None: error_filename = jube2.util.util.substitution( self._error_filename, parameter_dict) error_filename = \ os.path.expandvars(os.path.expanduser(error_filename)) if os.path.exists(os.path.join(work_dir, error_filename)): LOGGER.debug("Checking for error file \"{0}\" ..." .format(error_filename)) if jube2.conf.DEBUG_MODE: LOGGER.debug(" skip error") else: do = jube2.util.util.substitution(self._do, parameter_dict) raise(RuntimeError(("Error file \"{0}\" found after " + "running the command \"{1}\".").format( error_filename, do))) return continue_op, continue_cycle def etree_repr(self): """Return etree object representation""" do_etree = ET.Element("do") do_etree.text = self._do if self._async_filename is not None: do_etree.attrib["done_file"] = self._async_filename if self._error_filename is not None: do_etree.attrib["error_file"] = self._error_filename if self._break_filename is not None: do_etree.attrib["break_file"] = self._break_filename if self._stdout_filename is not None: do_etree.attrib["stdout"] = self._stdout_filename if self._stderr_filename is not None: do_etree.attrib["stderr"] = self._stderr_filename if self._active != "true": do_etree.attrib["active"] = self._active if self._shared: do_etree.attrib["shared"] = "true" if self._work_dir is not None: do_etree.attrib["work_dir"] = self._work_dir return do_etree def __repr__(self): return self._do @staticmethod def read_process_environment(work_dir, remove_after_read=True, pid=None): """Read standard environment info file in given directory.""" env = dict() last = None if pid is not None: env_file_name = jube2.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube2.conf.ENVIRONMENT_INFO env_file_path = os.path.join(work_dir, env_file_name) if os.path.isfile(env_file_path): env_file = open(env_file_path, "r") for line in env_file: line = line.rstrip() matcher = re.match(r"^(\S.*?)=(.*?)$", line) if matcher: env[matcher.group(1)] = matcher.group(2) last = matcher.group(1) elif last is not None: env[last] += "\n" + line env_file.close() if remove_after_read: os.remove(env_file_path) return env class DoLog(object): """A DoLog class containing the operations and information for setting up the do log.""" def __init__(self, log_dir, log_file, initial_env, cycle=0): self._log_dir = log_dir if log_file != None: if log_file[-1] == '/': raise ValueError( "The path of do_log_file is ending with / which is a invalid file path.") self._log_file = log_file self._initial_env = initial_env self._work_dir = None self._cycle = cycle self._log_path = None @property def log_path(self): """Get log directory""" return self._log_path @property def log_file(self): """Get log file""" return self._log_file @property def log_path(self): """Get log path""" return self._log_path @property def work_dir(self): """Get last work directory""" return self._work_dir @property def initial_env(self): """Get initial env""" return self._initial_env def initialiseFile(self, shell): """Initialise file if not yet existent.""" fdologout = open(self.log_path, 'a') fdologout.write('#!'+shell+'\n\n') for envVarName, envVarValue in self.initial_env.items(): fdologout.write('set '+envVarName+"='" + envVarValue.replace('\n', '\\n')+"'\n") fdologout.write('\n') fdologout.close() def store_do(self, do, shell, work_dir, parameter_dict=None, shared=False): """Store the current execution directive to the do log and set up the environment if file does not yet exist.""" if self._log_file == None: return if self._log_path == None: if parameter_dict: new_log_file = jube2.util.util.substitution( self._log_file, parameter_dict) new_log_file = os.path.expandvars( os.path.expanduser(new_log_file)) self._log_file = new_log_file if re.search(jube2.parameter.Parameter.parameter_regex, self._log_file): raise IOError(("Given do_log_file path {0} contains a unknown " + "JUBE or environment variable.").format( self._log_file)) if self._log_file[0] == '/': self._log_path = self._log_file elif '/' not in self._log_file: self._log_path = os.path.join(self._log_dir, self._log_file) else: self._log_path = os.path.join(os.getcwd(), self._log_file) # create directory if not yet existent if not os.path.exists(os.path.dirname(self.log_path)): os.makedirs(os.path.dirname(self.log_path)) if not os.path.exists(self.log_path): self.initialiseFile(shell) fdologout = open(self.log_path, 'a') if work_dir != self.work_dir: fdologout.write('cd '+work_dir+'\n') self._work_dir = work_dir fdologout.write(do) if shared: fdologout.write(' # shared execution') fdologout.write('\n') fdologout.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/substitute.py0000664000174700017470000001436114603772010020231 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Substitution related classes""" from __future__ import (print_function, unicode_literals, division) import os import re import jube2.util.util import jube2.util.output import jube2.conf import xml.etree.ElementTree as ET import jube2.log import shutil import codecs LOGGER = jube2.log.get_logger(__name__) class Substituteset(object): """A Substituteset contains all information""" def __init__(self, name, file_data, substitute_dict): self._name = name self._files = file_data self._substitute_dict = substitute_dict @property def name(self): """Return name of Substituteset""" return self._name def update_files(self, file_data): """Update iofiles""" outfiles = set([data[0] for data in self._files]) for data in file_data: if (data[2] == "a") or (data[0] not in outfiles): self._files.append(data) elif (data[2] == "w"): self._files = [fdat for fdat in self._files if fdat[0] != data[0]] self._files.append(data) def update_substitute(self, substitute_dict): """Update substitute_dict""" self._substitute_dict.update(substitute_dict) def substitute(self, parameter_dict=None, work_dir=None): """Do substitution. The work_dir can be set to a given context path. The parameter_dict used for inline substitution of destination-variables.""" if work_dir is None: work_dir = "" # Do pre-substitution of source and destination-variables if parameter_dict is not None: substitute_dict = dict() for name, sub in self._substitute_dict.items(): new_source = jube2.util.util.substitution(sub.source, parameter_dict) new_dest = jube2.util.util.substitution(sub.dest, parameter_dict) substitute_dict[new_source] = Sub(new_source, sub.mode, new_dest) else: substitute_dict = self._substitute_dict # Do file substitution for data in self._files: outfile_name = data[0] infile_name = data[1] out_mode = data[2] infile = jube2.util.util.substitution(infile_name, parameter_dict) outfile = jube2.util.util.substitution(outfile_name, parameter_dict) LOGGER.debug(" substitute {0} -> {1}".format(infile, outfile)) LOGGER.debug(" substitute:\n" + jube2.util.output.text_table( [("source", "dest")] + [(sub.source, sub.dest) for sub in substitute_dict.values()], use_header_line=True, indent=9, align_right=False)) if not jube2.conf.DEBUG_MODE: infile = os.path.join(work_dir, infile) outfile = os.path.join(work_dir, outfile) # Check not existing files if not (os.path.exists(infile) and os.path.isfile(infile)): raise RuntimeError(("File \"{0}\" not found while " "running substitution").format(infile)) # Read in-file file_handle = codecs.open(infile, "r", "utf-8") text = file_handle.read() file_handle.close() # Substitute for name, sub in substitute_dict.items(): if sub.mode == "text": text = text.replace(sub.source, sub.dest) else: text = re.sub(sub.source, sub.dest, text) # Write out-file file_handle = codecs.open(outfile, out_mode, "utf-8") file_handle.write(text) file_handle.close() if infile != outfile: shutil.copymode(infile, outfile) def etree_repr(self): """Return etree object representation""" substituteset_etree = ET.Element("substituteset") substituteset_etree.attrib["name"] = self._name for data in self._files: iofile_etree = ET.SubElement(substituteset_etree, "iofile") iofile_etree.attrib["in"] = data[1] iofile_etree.attrib["out"] = data[0] iofile_etree.attrib["out_mode"] = data[2] for name, sub in self._substitute_dict.items(): sub_etree = ET.SubElement(substituteset_etree, "sub") sub_etree.attrib["source"] = sub.source sub_etree.attrib["mode"] = sub.mode sub_etree.text = sub.dest return substituteset_etree def __repr__(self): return "Substitute({0})".format(self.__dict__) class Sub(object): def __init__(self, source, sub_mode, dest): self._source = source self._mode = sub_mode self._dest = dest @property def source(self): """Return source of Sub""" return self._source @property def mode(self): """Return type of Sub""" return self._mode @property def dest(self): """Return dest of Sub""" return self._dest def __eq__(self, other): return self._source == other.source def __hash__(self): return hash(self._source) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/jube2/util/0000775000174700017470000000000014603772011016415 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/util/__init__.py0000664000174700017470000000144214603772010020526 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2.util package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/util/output.py0000664000174700017470000001715614603772010020340 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ASCII and string output generators""" from __future__ import (print_function, unicode_literals, division) import jube2.conf import textwrap import copy import sys import xml.etree.ElementTree as ET def text_boxed(text): """Create an ASCII boxed version of text.""" box = "#" * jube2.conf.DEFAULT_WIDTH for line in text.split("\n"): box += "\n" lines = ["# {0}".format(element) for element in textwrap.wrap(line.strip(), jube2.conf.DEFAULT_WIDTH - 2)] if len(lines) == 0: box += "#" else: box += "\n".join(lines) box += "\n" + "#" * jube2.conf.DEFAULT_WIDTH return box def text_line(): """Return a horizonal ASCII line""" return "#" * jube2.conf.DEFAULT_WIDTH def text_table(entries_ext, use_header_line=False, indent=1, align_right=True, auto_linebreak=True, colw=None, style="pretty", separator=None, transpose=False): """Create a ASCII based table. entries must contain a list of lists, use_header_line can be used to mark the first entry as title. Return the ASCII table """ if style != "pretty": auto_linebreak = False use_header_line = False indent = 0 # Transpose data entries if needed if transpose: entries = list(zip(*entries_ext)) use_header_line = False else: entries = copy.deepcopy(entries_ext) max_length = list() table_str = "" header_line_used = not use_header_line # calculate needed maxlength for item in entries: for i, text in enumerate(item): if i > len(max_length) - 1: max_length.append(0) if style != "csv": for line in text.splitlines(): max_length[i] = max(max_length[i], len(line)) if auto_linebreak: max_length[i] = min(max_length[i], jube2.conf.MAX_TABLE_CELL_WIDTH) if colw is not None: for i, maxl in enumerate(max_length): if i < len(colw): max_length[i] = max(maxl, colw[i]) # fill cells for item in entries: # Wrap text wraps = list() for text in item: if auto_linebreak: lines = list() for line in text.splitlines(): lines += \ textwrap.wrap(line, jube2.conf.MAX_TABLE_CELL_WIDTH) wraps.append(lines) else: if style == "pretty": wraps.append(text.splitlines()) else: wraps.append([text.replace("\n", " ")]) grow = True height = 0 while grow: grow = False line_str = " " * indent if style == "pretty": line_str += "| " for i, wrap in enumerate(wraps): grow = grow or len(wrap) > height + 1 if len(wrap) > height: text = wrap[height] else: text = "" if align_right and height == 0: align = ">" else: align = "<" line_str += \ ("{0:" + align + str(max_length[i]) + "s}").format(text) if i < len(max_length) - 1: if separator is None: line_str += " | " if style == "pretty" else "," else: line_str += separator if style == "pretty": line_str += " |" line_str += "\n" table_str += line_str height += 1 if not header_line_used: # Create title separator line table_str += " " * indent + "|-" for i, cell_length in enumerate(max_length): table_str += "-" * cell_length if i < len(max_length) - 1: table_str += "-|-" table_str += "-|\n" header_line_used = True return table_str def print_loading_bar(current_cnt, all_cnt, wait_cnt=0, error_cnt=0): """Show a simple loading animation""" width = jube2.conf.DEFAULT_WIDTH - 10 cnt = dict() if all_cnt > 0: cnt["done_cnt"] = (current_cnt * width) // all_cnt cnt["wait_cnt"] = (wait_cnt * width) // all_cnt cnt["error_cnt"] = (error_cnt * width) // all_cnt else: cnt["done_cnt"] = 0 cnt["wait_cnt"] = 0 cnt["error_cnt"] = 0 # shrink cnt if there was some rounding issue for key in ("wait_cnt", "error_cnt"): if (cnt[key] > 0) and (width < sum(cnt.values())): cnt[key] = max(0, width - sum([cnt[k] for k in cnt if k != key])) # fill up medium_cnt if there was some rounding issue if (current_cnt + wait_cnt + error_cnt == all_cnt) and \ (sum(cnt.values()) < width): for key in ("wait_cnt", "error_cnt", "done_cnt"): if cnt[key] > 0: cnt[key] += width - sum(cnt.values()) break cnt["todo_cnt"] = width - sum(cnt.values()) bar_str = "\r{0}{1}{2}{3} ({4:3d}/{5:3d})".format("#" * cnt["done_cnt"], "0" * cnt["wait_cnt"], "E" * cnt["error_cnt"], "." * cnt["todo_cnt"], current_cnt, all_cnt) sys.stdout.write(bar_str) sys.stdout.flush() def element_tree_tostring(element, encoding=None): """A more encoding friendly ElementTree.tostring method""" class Dummy(object): """Dummy class to offer write method for etree.""" def __init__(self): self._data = list() @property def data(self): """Return data""" return self._data def write(self, *args): """Simulate write""" self._data.append(*args) file_dummy = Dummy() ET.ElementTree(element).write(file_dummy, encoding) return "".join(dat.decode(encoding) for dat in file_dummy.data) def format_value(format_string, value): """Return formated value""" if (type(value) is not int) and \ (("d" in format_string) or ("b" in format_string) or ("c" in format_string) or ("o" in format_string) or ("x" in format_string) or ("X" in format_string)): value = int(float(value)) elif (type(value) is not float) and \ (("e" in format_string) or ("E" in format_string) or ("f" in format_string) or ("F" in format_string) or ("g" in format_string) or ("G" in format_string)): value = float(value) format_string = "{{0:{0}}}".format(format_string) return format_string.format(value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/util/util.py0000664000174700017470000004144714603772010017755 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Utility functions, constants and classes""" from __future__ import (print_function, unicode_literals, division) from collections import deque import re import string import operator import os.path import subprocess import jube2.log import time import jube2.conf import grp import pwd LOGGER = jube2.log.get_logger(__name__) class Queue: ''' Queue based on collections.dequeue ''' def __init__(self): ''' Initialize this queue to the empty queue. ''' self._queue = deque() def put(self, item): ''' Add this item to the left of this queue. ''' self._queue.appendleft(item) def put_first(self, item): ''' Add this item to the left of this queue. ''' self._queue.append(item) def get_nowait(self): ''' Dequeues (i.e., removes) the item from the right side of this queue *and* returns this item. Raises ---------- IndexError If this queue is empty. ''' return self._queue.pop() def empty(self): ''' Return True if the queue is empty, False otherwise ''' return False if len(self._queue) > 0 else True class WorkStat(object): """Workpackage queuing handler""" def __init__(self): self._work_list = Queue() self._cnt_work = dict() self._wait_lists = dict() def put(self, workpackage): """Add some workpackage to queue""" # Substitute max_wps if needed max_wps = int(substitution(workpackage.step.max_wps, workpackage.parameter_dict)) if (max_wps == 0) or \ (workpackage.started) or \ (workpackage.step.name not in self._cnt_work) or \ (self._cnt_work[workpackage.step.name] < max_wps): self._work_list.put(workpackage) if workpackage.step.name not in self._cnt_work: self._cnt_work[workpackage.step.name] = 1 else: self._cnt_work[workpackage.step.name] += 1 else: if workpackage.step.name not in self._wait_lists: self._wait_lists[workpackage.step.name] = Queue() self._wait_lists[workpackage.step.name].put(workpackage) def update_queues(self, last_workpackage): """Check if a workpackage can move from waiting to work queue""" if last_workpackage.done: self._cnt_work[last_workpackage.step.name] -= 1 if (last_workpackage.step.name in self._wait_lists) and \ (not self._wait_lists[last_workpackage.step.name].empty()): workpackage = \ self._wait_lists[last_workpackage.step.name].get_nowait() # Check if workpackage was started from another position if not workpackage.started: self.put(workpackage) else: self.update_queues(last_workpackage) def get(self): """Get some workpackage from work queue""" return self._work_list.get_nowait() def empty(self): """Check if work queue is empty""" return self._work_list.empty() def push_back(self, wp): """push element to the first position of the queue""" self._work_list.put_first(wp) def valid_tags(tag_string, tags): """Check if tag_string contains only valid tags""" if tags is None: tags = set() tag_tags_str = tag_string if tag_tags_str is not None: # Check for old tag format if "," in tag_tags_str: tag_tags_str = jube2.jubeio.Parser._convert_old_tag_format( tag_tags_str) tag_tags_str = tag_tags_str.replace(' ', '') tag_array = [i for i in re.split('[()|+!]', tag_tags_str) if len(i) > 0] tag_state = {} for tag in tag_array: tag_state.update({tag: str(tag in tags)}) for tag in tag_array: tag_tags_str = re.sub(r'(?:^|(?<=\W))' + tag + r'(?=\W|$)', tag_state[tag], tag_tags_str) tag_tags_str = tag_tags_str.replace('|', ' or ')\ .replace('+', ' and ').replace('!', ' not ') try: return eval(tag_tags_str) except SyntaxError: raise ValueError("Tag string '{0}' not parseable." .format(tag_string)) else: return True def get_current_id(base_dir): """Return the highest id found in directory 'base_dir'.""" try: filelist = sorted(os.listdir(base_dir)) except OSError as error: LOGGER.warning(error) filelist = list() maxi = -1 for item in filelist: try: maxi = max(int(re.findall("^([0-9]+)$", item)[0]), maxi) except IndexError: pass return maxi def id_dir(base_dir, id_number): """Return path for 'id_number' in 'base_dir'.""" return os.path.join( base_dir, "{id_number:0{zfill}d}".format(zfill=jube2.conf.ZERO_FILL_DEFAULT, id_number=id_number)) def expand_dollar_count(text): # Replace a even number of $ by $$$$, because they will be # substituted to $$. Even number will stay the same, odd number # will shrink in every turn # $$ -> $$$$ -> $$ # $$$ -> $$$ -> $ # $$$$ -> $$$$$$$$ -> $$$$ # $$$$$ -> $$$$$$$ -> $$$ return re.sub(r"(^(?=\$)|[^$])((?:\$\$)+?)((?:\${3})?(?:[^$]|$))", r"\1\2\2\3", text) def substitution(text, substitution_dict): """Substitute templates given by parameter_dict inside of text""" changed = True count = 0 # All values must be string values (handle Python 2 separatly) try: str_substitution_dict = \ dict([(k, str(v).decode("utf-8", errors="ignore")) for k, v in substitution_dict.items()]) except TypeError: str_substitution_dict = \ dict([(k, str(v).decode("utf-8", "ignore")) for k, v in substitution_dict.items()]) except AttributeError: str_substitution_dict = dict([(k, str(v)) for k, v in substitution_dict.items()]) # Preserve non evaluated parameter before starting substitution local_substitution_dict = dict([(k, re.sub(r"\$", "$$", v) if "$" in v else v) for k, v in str_substitution_dict.items()]) # Run multiple times to allow recursive parameter substitution while changed and count < jube2.conf.MAX_RECURSIVE_SUB: count += 1 orig_text = text # Save double $$ text = expand_dollar_count(text) \ if "$" in text else text tmp = string.Template(text) new_text = tmp.safe_substitute(local_substitution_dict) changed = new_text != orig_text text = new_text # Final substitution to remove $$ tmp = string.Template(text) return re.sub("\$(?=([\s]|$))","$$",tmp.safe_substitute(str_substitution_dict)) def convert_type(value_type, value, stop=True): """Convert value to given type""" result_value = None value_type_incorrect=False try: if value_type == "int": if value == "nan": result_value = float("nan") else: result_value = int(float(value)) if re.match(r"^[-+]?\d+$", value) is None: value_type_incorrect=True elif value_type == "float": result_value = float(value) if re.match(r"([+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))",value) is None: value_type_incorrect=True else: result_value = value except ValueError: if stop: raise ValueError(f"\"{value}\" cannot be represented as a \"{value_type}\"") else: result_value = value if value_type_incorrect: print(f"Warning: \"{value}\" was converted to type \"{value_type}\": {result_value}.\n") LOGGER.debug(f"Warning: \"{value}\" was converted to type \"{value_type}\": {result_value}.\n") return result_value def script_evaluation(cmd, script_type): """cmd will be evaluated with given script language""" if script_type == "python": return str(eval(cmd)) elif script_type in ["perl", "shell"]: if script_type == "perl": cmd = "perl -e \"print " + cmd + "\"" # Select unix shell shell = jube2.conf.STANDARD_SHELL if "JUBE_EXEC_SHELL" in os.environ: alt_shell = os.environ["JUBE_EXEC_SHELL"].strip() if len(alt_shell) > 0: shell = alt_shell sub = subprocess.Popen([shell, "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) stdout, stderr = sub.communicate() stdout = stdout.decode(errors="ignore") # Check command execution error code errorcode = sub.wait() if errorcode != 0: raise RuntimeError(stderr) else: if len(stderr.strip()) > 0: try: LOGGER.debug((" The command \"{0}\" was executed with a " "successful error code,\n but the " "following error message was produced " "during its execution: {1}") .format(cmd, stderr)) except UnicodeDecodeError: pass return stdout def eval_bool(cmd): """Evaluate a bool expression""" if cmd.lower() == "true": return True elif cmd.lower() == "false": return False else: try: return bool(eval(cmd)) except SyntaxError as se: raise ValueError( ("\"{0}\" could not be evaluated and handled as boolean " "value. Check if all parameter were correctly replaced and " "the syntax of the expression is well formed ({1}).").format( cmd, str(se))) def get_tree_element(node, tag_path=None, attribute_dict=None): """Can be used instead of node.find(.//tag_path[@attrib=value])""" result = get_tree_elements(node, tag_path, attribute_dict) if len(result) > 0: return result[0] else: return None def get_tree_elements(node, tag_path=None, attribute_dict=None): """Can be used instead of node.findall(.//tag_path[@attrib=value])""" if attribute_dict is None: attribute_dict = dict() result = list() if tag_path is not None: node_list = node.findall(tag_path) else: node_list = [node] for found_node in node_list: for attribute, value in attribute_dict.items(): if found_node.get(attribute) != value: break else: result.append(found_node) for subtree in node: result += get_tree_elements(subtree, tag_path, attribute_dict) return result def now_str(): """Return current time string""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def update_timestamps(path, *args): """Set all timestamps for given arg_names to now""" timestamps = dict() timestamps.update(read_timestamps(path)) file_ptr = open(path, "w") for arg in args: timestamps[arg] = now_str() for timestamp in timestamps: file_ptr.write("{0}: {1}\n".format(timestamp, timestamps[timestamp])) file_ptr.close() def read_timestamps(path): """Return timestamps dictionary""" timestamps = dict() if os.path.isfile(path): file_ptr = open(path, "r") for line in file_ptr: matcher = re.match("(.*?): (.*)", line.strip()) if matcher: timestamps[matcher.group(1)] = matcher.group(2) file_ptr.close() return timestamps def resolve_depend(depend_dict): """Generate a serialization of dependent steps. Return a list with a possible order of execution. """ def find_next(dependencies, finished): """Returns the next possible items to be processed and remainder. dependencies Dictionary containing the dependencies finished Set which is already processed """ possible = set() remain = dict() for key, val in dependencies.items(): if val.issubset(finished): possible.add(key) else: remain[key] = val possible.difference_update(finished) # no advance if dependencies and not possible: unresolved_steps = set(dependencies) - finished unresolved_dependencies = set() for step in unresolved_steps: unresolved_dependencies.update(depend_dict[step] - finished) infostr = ("unresolved steps: {0}". format(",".join(unresolved_steps)) + "\n" + "unresolved dependencies: {0}". format(",".join(unresolved_dependencies))) LOGGER.warning(infostr) return (possible, remain) finished = set() work_list = list() work, remain = find_next(depend_dict, finished) while work: work_list += list(work) finished.update(work) work, remain = find_next(remain, finished) return work_list def check_and_get_group_id(): """Read environment var JUBE_GROUP_NAME and return group id""" group_name = "" if "JUBE_GROUP_NAME" in os.environ: group_name = os.environ["JUBE_GROUP_NAME"].strip() if group_name != "": try: group_id = grp.getgrnam(group_name).gr_gid except KeyError: raise ValueError(("Failed to get group ID, group \"{0}\" " + "does not exist").format(group_name)) user = pwd.getpwuid(os.getuid()).pw_name grp_members = grp.getgrgid(group_id).gr_mem if user in grp_members: return group_id else: raise ValueError(("User \"{0}\" is not in " + "group \"{1}\"").format(user, group_name)) else: return None def consistency_check(benchmark): """Do some consistency checks""" # check if step uses exists for step in benchmark.steps.values(): for uses in step.use: for use in uses: if (use not in benchmark.parametersets) and \ (use not in benchmark.filesets) and \ (use not in benchmark.substitutesets) and \ ("$" not in use): raise ValueError(("{0} not found in " "available sets").format(use)) # Dependency check depend_dict = \ dict([(step.name, step.depend) for step in benchmark.steps.values()]) order = resolve_depend(depend_dict) for step_name in benchmark.steps: if step_name not in order: raise ValueError("Cannot resolve dependencies.") class CompType(object): """Allow comparison of different datatypes""" def __init__(self, value): self.__value = value def __repr__(self): return str(self.__value) @property def value(self): return self.__value def _special_comp(self, other, comp_func): """Allow comparision of different datatypes""" if self.value is None or other.value is None: return False else: try: return comp_func(self.value, other.value) except TypeError: return False def __lt__(self, other): return self._special_comp(other, operator.lt) def __eq__(self, other): return self._special_comp(other, operator.eq) def safe_split(text, separator): """Like split for non-empty separator, list with text otherwise.""" if separator: return text.split(separator) else: return [text] def ensure_list(element): if type(element)!=list: return [element] else: return element ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/util/version.py0000664000174700017470000001502214603772010020453 0ustar00gitlab-runnergitlab-runner# Updated from: # distutils/version.py # # found on: https://github.com/pypa/distutils/blob/main/distutils/version.py # # Implements multiple version numbering conventions for the # Python Module Distribution Utilities. # # $Id$ # """Provides classes to represent module version numbers (one class for each style of version numbering). There are currently two such classes implemented: StrictVersion and LooseVersion. Every version number class implements the following interface: * the 'parse' method takes a string and parses it to some internal representation; if the string is an invalid version number, 'parse' raises a ValueError exception * the class constructor takes an optional string argument which, if supplied, is passed to 'parse' * __str__ reconstructs the string that was passed to 'parse' (or an equivalent string -- ie. one that will generate an equivalent version number instance) * __repr__ generates Python code to recreate the version number instance * _cmp compares the current instance with either another instance of the same class or a string (which will be parsed to an instance of the same class, thus must follow the same rules) """ import re class Version: """Abstract base class for version numbering classes. Just provides constructor (__init__) and reproducer (__repr__), because those seem to be the same for all version numbering classes; and route rich comparisons to _cmp. """ def __init__(self, vstring=None): if vstring: self.parse(vstring) def __repr__(self): return f"{self.__class__.__name__} ('{str(self)}')" def __eq__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c == 0 def __lt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c < 0 def __le__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c <= 0 def __gt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c > 0 def __ge__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c >= 0 # Interface for version-number classes -- must be implemented # by the following classes (the concrete ones -- Version should # be treated as an abstract class). # __init__ (string) - create and take same action as 'parse' # (string parameter is optional) # parse (string) - convert a string representation to whatever # internal representation is appropriate for # this style of version numbering # __str__ (self) - convert back to a string; should be very similar # (if not identical to) the string supplied to parse # __repr__ (self) - generate Python code to recreate # the instance # _cmp (self, other) - compare two version numbers ('other' may # be an unparsed version string, or another # instance of your version class) class StrictVersion(Version): """Version numbering for anal retentives and software idealists. Implements the standard interface for version number classes as described above. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of the letter 'a' or 'b' followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without. The following are valid version numbers (shown in the order that would be obtained by sorting according to the supplied cmp function): 0.4 0.4.0 (these two are equivalent) 0.4.1 0.5a1 0.5b3 0.5 0.9.6 1.0 1.0.4a3 1.0.4b1 1.0.4 The following are examples of invalid version numbers: 1 2.7.2.2 1.3.a4 1.3pl1 1.3c4 The rationale for this version numbering system will be explained in the distutils documentation. """ version_re = re.compile( r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', re.VERBOSE | re.ASCII ) def parse(self, vstring): match = self.version_re.match(vstring) if not match: raise ValueError(f"invalid version number '{vstring}'") (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6) if patch: self.version = tuple(map(int, [major, minor, patch])) else: self.version = tuple(map(int, [major, minor])) + (0,) if prerelease: self.prerelease = (prerelease[0], int(prerelease_num)) else: self.prerelease = None def __str__(self): if self.version[2] == 0: vstring = '.'.join(map(str, self.version[0:2])) else: vstring = '.'.join(map(str, self.version)) if self.prerelease: vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) return vstring def _cmp(self, other): # noqa: C901 if isinstance(other, str): other = StrictVersion(other) elif not isinstance(other, StrictVersion): return NotImplemented if self.version != other.version: # numeric versions don't match # prerelease stuff doesn't matter if self.version < other.version: return -1 else: return 1 # have to compare prerelease # case 1: neither has prerelease; they're equal # case 2: self has prerelease, other doesn't; other is greater # case 3: self doesn't have prerelease, other does: self is greater # case 4: both have prerelease: must compare them! if not self.prerelease and not other.prerelease: return 0 elif self.prerelease and not other.prerelease: return -1 elif not self.prerelease and other.prerelease: return 1 elif self.prerelease and other.prerelease: if self.prerelease == other.prerelease: return 0 elif self.prerelease < other.prerelease: return -1 else: return 1 else: assert False, "never get here" # end class StrictVersion ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/util/yaml_converter.py0000664000174700017470000003110614603772010022020 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """YAML to XML converter""" from __future__ import (print_function, unicode_literals, division) import xml.etree.ElementTree as etree import xml.dom.minidom as DOM try: import ruamel.yaml except ImportError: pass try: import yaml except ImportError: pass import jube2.log import jube2.conf import jube2.util.output import os import copy import jube2.util.util try: from StringIO import StringIO as IOStream except ImportError: from io import BytesIO as IOStream LOGGER = jube2.log.get_logger(__name__) class YAML_Converter(object): """YAML to XML converter""" allowed_tags = \ {"/": ["benchmark", "parameterset", "comment", "step", "fileset", "substituteset", "analyser", "result", "patternset", "selection", "include-path", "check_tags"], "/benchmark": ["benchmark", "parameterset", "fileset", "substituteset", "patternset", "selection", "include-path", "check_tags"], "benchmark": ["parameterset", "comment", "step", "fileset", "substituteset", "analyser", "result", "patternset"], "analyse": ["file"], "analyser": ["use", "analyse"], "fileset": ["link", "copy", "prepare"], "include-path": ["path"], "parameterset": ["parameter"], "patternset": ["pattern"], "result": ["use", "table", "syslog", "database"], "selection": ["not", "only", "tag"], "step": ["use", "do"], "substituteset": ["iofile", "sub"], "syslog": ["key"], "table": ["column"], "database": ["key"]} def __init__(self, path, include_path=None, tags=None): self._path = path if include_path is None: include_path = [] if tags is None: tags = set() self._include_path = list(include_path) self._include_path += [os.path.dirname(self._path)] self._tags = set(tags) try: yaml.add_constructor("!include", self.__yaml_include) except NameError: raise NameError("yaml module not available; either install it " + "(https://pyyaml.org), or switch to .xml input " + "files.") self._ignore_search_errors = True self._tags.update(self.__search_for_tags()) old_tags = set(self._tags) changed = True counter = 0 # It is possible to add new tags by including external files into a # selection block therefore the input must be scanned multiple times # to gather all available tags while changed and counter < jube2.conf.PREPROCESS_MAX_ITERATION: self._include_path = list(include_path) + \ self.__search_for_include_pathes() + \ [os.path.dirname(self._path)] self._tags.update(self.__search_for_tags()) changed = len(self._tags.difference(old_tags)) > 0 old_tags = set(self._tags) counter += 1 self._ignore_search_errors = False self._int_file = IOStream() self.__convert() def __convert(self): """ Opens given file, make a Tree of it and print it """ # Check the validity of the yaml file with open(self._path, "r") as file_handle: try: ruamel.yaml.YAML().load(file_handle) except NameError: pass except ruamel.yaml.constructor.DuplicateKeyError as e: e.note="" raise(e) LOGGER.debug(" Start YAML to XML file conversion for file {0}".format( self._path)) # Read the yaml file and create an xml tree with open(self._path, "r") as file_handle: xmltree = etree.Element('jube') data = yaml.load(file_handle.read(), Loader=yaml.Loader) YAML_Converter.create_headtags(data, xmltree, self._include_path) xml = jube2.util.output.element_tree_tostring( xmltree, encoding="UTF-8") self._int_file.write(xml.encode('UTF-8')) LOGGER.debug(" YAML Conversion finalized") def read(self): """Read data of converted file""" return self._int_file.getvalue() def close(self): """Close converted file""" self._int_file.close() def __find_include_file(self, filename): """Search for filename in include-pathes and return resulting path""" for path in self._include_path: file_path = os.path.join(path, filename) if os.path.exists(file_path): break else: raise ValueError(("\"{0}\" not found in possible " + "include pathes").format(filename)) return file_path def __search_for_tags(self): """Search a YAML file for stored tag information""" tags = set() with open(self._path, "r") as file_handle: data = yaml.load(file_handle.read(), Loader=yaml.Loader) if "selection" in data and "tag" in data["selection"]: if type(data["selection"]["tag"]) is not list: data["selection"]["tag"] = [data["selection"]["tag"]] for tag in data["selection"]["tag"]: if not tag.startswith("!include "): tags.update( set(tag.split(jube2.conf.DEFAULT_SEPARATOR))) return tags def __search_for_include_pathes(self): """Search a YAML file for stored include-path information""" include_pathes = [] with open(self._path, "r") as file_handle: data = yaml.load(file_handle.read(), Loader=yaml.Loader) # include-path is only allowed on the top level of the tree if "include-path" in data: if type(data["include-path"]) is not list: data["include-path"] = [data["include-path"]] values = self.__search_for_pathes(data["include-path"]) for val in values: include_pathes.append(os.path.join( os.path.dirname(self._path), val)) return include_pathes def __search_for_pathes(self, data): """Search in given data for stored path informations""" paths = [] for path in data: if type(path) is dict: if "tag" in path and not jube2.util.util.valid_tags(path["tag"], self._tags): return value = path["path"] if "path" in path else path["_"] if type(value) is not list: value = [value] path_val = self.__search_for_pathes(value) paths.extend(path_val) elif type(path) is list: path_val = self.__search_for_pathes(path) paths.extend(path_val) else: paths.append(path) return paths # adapted from # http://code.activestate.com/recipes/577613-yaml-include-support/ def __yaml_include(self, loader, node): """ Constructor for the include tag""" yaml_node_data = node.value.split(":") try: file = self.__find_include_file(yaml_node_data[0]) if os.path.normpath(file) == os.path.normpath(self._path): # Avoid recursive !include loops loader = yaml.BaseLoader else: loader = yaml.Loader with open(file) as inputfile: try: _ = yaml.load(inputfile.read(), Loader=loader) except yaml.parser.ParserError: LOGGER.error(("Including data from \"{0}\" into \"{1}\" " + "raised an error.").format(file, self._path)) raise inputfile.close() if len(yaml_node_data) > 1: _ = eval("_" + yaml_node_data[1]) if len(yaml_node_data) > 2: _ = eval(yaml_node_data[2]) return _ except ValueError as ve: if self._ignore_search_errors: return "!include {0}".format(node.value) else: raise ve @staticmethod def create_headtags(data, parent_node, include_pathes): """ Search for the headtags in given dictionary """ if type(data) is not dict: data = {'benchmark': data} to_delete = list() for tag in data.keys(): # Override include-path with parsed include-path if tag == "include-path": data[tag] = include_pathes if type(data[tag]) is not list: data[tag] = [data[tag]] # benchmark is optional on the top level, but if it is used only # a limited number of options are allowed on top level # (listed in "/benchmark") if "benchmark" in data and tag in YAML_Converter.allowed_tags[ "/benchmark"]: for attr_and_tags in data[tag]: YAML_Converter.create_tag(tag, attr_and_tags, parent_node) elif "benchmark" not in data and \ tag in YAML_Converter.allowed_tags["/"]: if tag not in YAML_Converter.allowed_tags["benchmark"]: for attr_and_tags in data[tag]: YAML_Converter.create_tag( tag, attr_and_tags, parent_node) to_delete.append(tag) for tag in to_delete: del(data[tag]) if "benchmark" not in data: YAML_Converter.create_tag("benchmark", data, parent_node) @staticmethod def create_tag(new_node_name, data, parent_node): """ Create the Subtag name, search for known tags and set the given attributes""" LOGGER.debug(" Create XML tag <{0}>".format(new_node_name)) new_node = etree.SubElement(parent_node, new_node_name) # Check if tag can have subtags if new_node_name in YAML_Converter.allowed_tags and type(data) is dict: allowed_tags = YAML_Converter.allowed_tags[new_node_name] for key, value in data.items(): if (type(value) is not list): value = [value] for val in value: if key in allowed_tags: # Create new subtag YAML_Converter.create_tag(key, val, new_node) else: # Create attribute new_node.set(key, str(val) if val is not None else "") else: tag_value = "" if type(data) is not dict: # standard tag value tag_value = data if data is not None else "" else: for key, value in data.items(): if key == "_": # _ represents the standard tag value tag_value = value if value is not None else "" else: # Create attribute new_node.set(key, str(value) if value is not None else "") if type(tag_value) is list: new_node.text = str(tag_value.pop(0)) while len(tag_value) > 0: new_node = copy.deepcopy(new_node) parent_node.append(new_node) new_node.text = str(tag_value.pop(0)) else: new_node.text = str(tag_value) @staticmethod def is_parseable_yaml_file(filename): try: with open(filename, "r") as file_handle: if type(yaml.load(file_handle.read())) is str: return False else: return True except Exception as parseerror: return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/jube2/workpackage.py0000664000174700017470000011302114603772010020305 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Workpackage class handles a step and its parameter space""" from __future__ import (print_function, unicode_literals, division) import multiprocessing as mp import xml.etree.ElementTree as ET import jube2.util.util import jube2.util.output import jube2.conf import jube2.log import jube2.parameter import jube2.step import os import re import stat import shutil LOGGER = jube2.log.get_logger(__name__) class Workpackage(object): """A Workpackage contains all information to run a specific step with its given parameterset. """ # class based counter for unique id creation id_counter = 0 def __init__(self, benchmark, step, local_parameter_names, parameterset, workpackage_id=None, iteration=0, cycle=0): # set id if workpackage_id is None: self._id = Workpackage.id_counter Workpackage.id_counter = Workpackage.id_counter + 1 else: self._id = workpackage_id self._benchmark = benchmark self._step = step self._local_parameter_names = local_parameter_names self._parameterset = parameterset self._iteration = iteration self._parents = list() self._children = list() self._iteration_siblings = set() self._queued = False self._env = dict(os.environ) self._cycle = cycle self._workpackage_dir_caching_enabled = False self._workpackage_dir_cache = None def etree_repr(self): """Return etree object representation""" workpackage_etree = ET.Element("workpackage") workpackage_etree.attrib["id"] = str(self._id) step_etree = ET.SubElement(workpackage_etree, "step") step_etree.attrib["iteration"] = str(self._iteration) step_etree.attrib["cycle"] = str(self._cycle) step_etree.text = self._step.name if len(self._local_parameter_names) > 0: workpackage_etree.append( self.local_parameterset.etree_repr(use_current_selection=True)) if len(self._parents) > 0: parents_etree = ET.SubElement(workpackage_etree, "parents") parents_etree.text = ",".join( [str(parent.id) for parent in self._parents]) if len(self._iteration_siblings) > 0: sibling_etree = ET.SubElement(workpackage_etree, "iteration_siblings") sibling_etree.text = ",".join( [str(sibling.id) for sibling in self._iteration_siblings]) environment_etree = ET.SubElement(workpackage_etree, "environment") for env_name, value in self._env.items(): if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in os.environ or os.environ[env_name] != value): env_etree = ET.SubElement(environment_etree, "env") env_etree.attrib["name"] = env_name # use string repr to avoid special characters env_etree.text = repr(value) for env_name in os.environ: if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in self._env): env_etree = ET.SubElement(environment_etree, "nonenv") env_etree.attrib["name"] = env_name return workpackage_etree def __repr__(self): return (("Workpackage(Id:{0:2d}; Step:{1}; ParentIDs:{2}; " + "ChildIDs:{3} {4})"). format(self._id, self._step.name, [parent.id for parent in self._parents], [child.id for child in self._children], self.local_parameterset)) def __eq__(self, other): if isinstance(other, Workpackage): return self.id == other.id else: return False def __hash__(self): return object.__hash__(self) @property def parameter_dict(self): """get all available parameter inside a dict""" # Collect parameter for substitution parameter = dict([[par.name, par.value] for par in self._parameterset.constant_parameter_dict.values()]) return parameter @property def env(self): """Return workpackage environment""" return self._env @env.setter def env(self, set_env): """Replace own environment by set_env""" self._env = set_env @property def cycle(self): """Return current loop cycle""" return self._cycle @cycle.setter def cycle(self, set_cycle): """Update loop cycle counter""" self._cycle = set_cycle def allow_workpackage_dir_caching(self): """Enable workpackage dir cache""" self._workpackage_dir_caching_enabled = True self._workpackage_dir_cache = None @property def active(self): """Check active state""" active = self._step.active # Collect parameter for substitution parameter = self.parameter_dict # Parameter substitution active = jube2.util.util.substitution(active, parameter) # Evaluate active state return jube2.util.util.eval_bool(active) @property def done(self): """Workpackage done?""" done_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) exist = os.path.exists(done_file) if jube2.conf.DEBUG_MODE: exist = exist or os.path.exists(done_file + "_DEBUG") return exist @done.setter def done(self, set_done): """Set/reset Workpackage done""" done_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) if jube2.conf.DEBUG_MODE: done_file = done_file + "_DEBUG" if set_done: fout = open(done_file, "w") fout.write(jube2.util.util.now_str()) fout.close() self._remove_operation_info_files() else: if os.path.exists(done_file): os.remove(done_file) @property def error(self): """Workpackage error?""" error_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_ERROR_FILENAME) return os.path.exists(error_file) def set_error(self, set_error, msg=""): """Set/reset Workpackage error""" error_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_ERROR_FILENAME) if set_error: fout = open(error_file, "w") fout.write(msg) fout.close() else: if os.path.exists(error_file): os.remove(error_file) @property def queued(self): """Workpackage queued?""" return self._queued @queued.setter def queued(self, set_queued): """Set queued state""" self._queued = set_queued @property def started(self): """Workpackage started?""" return os.path.exists(self.workpackage_dir) def operation_done_but_pending(self, operation_number): """Check if an operation was executed, but the result is still pending (because it is a async do)""" result = self.operation_done(operation_number) operation = self._step.operations[operation_number] if result and (operation.async_filename is not None): parameter_dict = self.parameter_dict if operation.active(parameter_dict): work_dir = self.work_dir alt_work_dir = self.alt_work_dir(parameter_dict) if alt_work_dir is not None: work_dir = alt_work_dir async_filename = jube2.util.util.substitution( operation.async_filename, parameter_dict) async_filename = \ os.path.expandvars(os.path.expanduser(async_filename)) result = not os.path.exists(os.path.join(work_dir, async_filename)) else: result = False else: result = False return result def operation_done(self, operation_number, set_done=None): """Mark/checks operation status""" done_file = os.path.join(self.workpackage_dir, "wp_{0}_{1:02d}".format( jube2.conf.WORKPACKAGE_DONE_FILENAME, operation_number)) if set_done is None: exist = os.path.exists(done_file) if jube2.conf.DEBUG_MODE: exist = exist or os.path.exists(done_file + "_DEBUG") return exist else: if jube2.conf.DEBUG_MODE: done_file = done_file + "_DEBUG" elif ((set_done and not os.path.exists(done_file)) or (not set_done and os.path.exists(done_file))): jube2.util.util.update_timestamps( os.path.join(self._benchmark.bench_dir, jube2.conf.TIMESTAMPS_INFO), "change") if set_done: fout = open(done_file, "w") fout.close() else: if os.path.exists(done_file): os.remove(done_file) return set_done def _remove_operation_info_files(self): """Remove all operation info files""" for operation_number in range(len(self._step.operations)): self.operation_done(operation_number, False) def remove(self, remove_config_from_benchmark=False): """Remove all data of this workpackage""" for children in self.children: children.remove(remove_config_from_benchmark=True) shutil.rmtree(self.workpackage_dir, ignore_errors=True) # Remove shared folder if all workpackages of the current step were # removed if self._step.shared_link_name is not None: all_deleted = True for workpackage in self._benchmark.workpackages[self._step.name]: if workpackage.started: all_deleted = False if all_deleted: shared_folder = self._step.shared_folder_path( self._benchmark.bench_dir, self.parameter_dict) shutil.rmtree(shared_folder, ignore_errors=True) if remove_config_from_benchmark: self.benchmark.remove_workpackage(self) def add_parent(self, workpackage): """Add a parent Workpackage""" self._parents.append(workpackage) @property def parameterset(self): """Return parameterset""" return self._parameterset @parameterset.setter def parameterset(self, set_parameterset): """Set/overwrite parameterset""" self._parameterset.add_parameterset(set_parameterset) def add_children(self, workpackage): """Add a children workpackage""" self._children.append(workpackage) @property def local_parameterset(self): """Return local parameterset""" parameterset = jube2.parameter.Parameterset() for name in self._local_parameter_names: parameterset.add_parameter(self._parameterset[name]) return parameterset @property def parent_history(self): """Create a list of all parents in the history of this workpackage""" history = list() for parent in self._parents: history += parent.parent_history history += self._parents return history @property def benchmark(self): """Return benchmark of this workpackage""" return self._benchmark @property def children_future(self): """Create a list of all children in the future of this workpackage""" future = list() future += self._children for child in self._children: future += child.children_future return future @property def id(self): """Return workpackage id""" return self._id @property def parents(self): """Return list of parent workpackages""" return self._parents @property def iteration_siblings(self): """Return set of iteration siblings""" return self._iteration_siblings @property def iteration(self): """Return workpackage iteration number""" return self._iteration @property def children(self): """Return list of child workpackages""" return self._children @property def step(self): """Return Step data""" return self._step def status(self): """return FINISHED, RUNNING or DONE dependign on the workpackage status""" if self.done: return "DONE" elif self.error: return "ERROR" else: return "RUNNING" def update_status(self): """Update status in jube parameter""" parameterset = jube2.parameter.Parameterset() parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_status", self.status(), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) self.parameterset.update_parameterset(parameterset) def get_jube_cycle_parameterset(self): """Return parameterset which contains cycle related information""" parameterset = jube2.parameter.Parameterset() # worpackage cycle parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_cycle", str(self._cycle), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_relpath(self, value): """Create relative path representation""" return os.path.relpath(value, self._benchmark.file_path_ref) def create_abspath(self, value): """Create absolute path representation""" return os.path.abspath(value) def get_jube_parameterset(self): """Return parameterset which contains workpackage related information""" parameterset = jube2.parameter.Parameterset() # workpackage id parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_id", str(self._id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # workpackage id with padding parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_padid", jube2.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) # workpackage status parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_status", self.status(), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) # workpackage iteration parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_iteration", str(self._iteration), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) parameterset.add_parameterset(self.get_jube_cycle_parameterset()) # pathes if self._step.alt_work_dir is None: path = self.work_dir else: path = self._step.alt_work_dir # workpackage relative folder path parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_relpath", path, update_mode=jube2.parameter.JUBE_MODE, eval_helper=self.create_relpath)) # workpackage absolute folder path parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_abspath", path, update_mode=jube2.parameter.JUBE_MODE, eval_helper=self.create_abspath)) # parent workpackage id for parent in self._parents: parameterset.add_parameter( jube2.parameter.Parameter. create_parameter(("jube_wp_parent_{0}_id") .format(parent.step.name), str(parent.id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # environment export string env_str = "" parameter_names = [parameter.name for parameter in self._parameterset.export_parameter_dict.values()] parameter_names.sort(key=str.lower) for name in parameter_names: env_str += "export {0}=${1}\n".format(name, name) env_par = jube2.parameter.Parameter.create_parameter( "jube_wp_envstr", env_str, no_templates=True, update_mode=jube2.parameter.JUBE_MODE, eval_helper=jube2.parameter.StaticParameter.fix_export_string) parameterset.add_parameter(env_par) # environment export list parameterset.add_parameter( jube2.parameter.Parameter.create_parameter( "jube_wp_envlist", ",".join([name for name in parameter_names]), no_templates=True, update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_workpackage_dir(self): """Create work directory""" if not os.path.exists(self.workpackage_dir): if "$" in self.workpackage_dir: raise RuntimeError(("'{0}' could not be evaluated and used " + "as a workpackage directory name. " + "Please check the suffix setting.") .format(self.workpackage_dir)) os.mkdir(self.workpackage_dir) os.mkdir(self.work_dir) # Create symbolic link to parent workpackage folder for parent in self._parents: link_path = os.path.join(self.work_dir, parent.step.name) parent_path = os.path.relpath(parent.work_dir, self.work_dir) if not os.path.exists(link_path): os.symlink(parent_path, link_path) def create_shared_folder_link(self, parameter_dict=None): """Create shared folder connection""" # Create symbolic link to shared folder if self._step.shared_link_name is not None: shared_folder = self._step.shared_folder_path( self._benchmark.bench_dir, parameter_dict) # Create shared folder (if it not already exists) if not os.path.exists(shared_folder): try: os.mkdir(shared_folder) except FileExistsError: pass # Create shared folder link if parameter_dict is not None: shared_name = \ jube2.util.util.substitution(self._step.shared_link_name, parameter_dict) else: shared_name = self._step.shared_link_name link_path = os.path.join(self.work_dir, shared_name) target_path = \ os.path.relpath(shared_folder, self.work_dir) if not os.path.exists(link_path): os.symlink(target_path, link_path) @property def workpackage_dir(self): """Return workpackage directory""" if not self._workpackage_dir_caching_enabled or \ self._workpackage_dir_cache is None: suffix = self.step.suffix if suffix != "": # Collect parameter for substitution parameter = \ dict([[par.name, par.value] for par in self._parameterset.constant_parameter_dict.values()]) # Parameter substitution suffix = jube2.util.util.substitution(suffix, parameter) suffix = "_" + os.path.expandvars(os.path.expanduser(suffix)) path = "{path}_{step_name}{suffix}".format( path=jube2.util.util.id_dir( self._benchmark.bench_dir, self._id), step_name=self._step.name, suffix=suffix) if self._workpackage_dir_caching_enabled: if self._workpackage_dir_cache is None: self._workpackage_dir_cache = path return self._workpackage_dir_cache else: return path @property def work_dir(self): """Return working directory (user space)""" return os.path.join(self.workpackage_dir, "work") def alt_work_dir(self, parameter_dict=None): """Return location of alternative working_dir""" if self._step.alt_work_dir is not None: if parameter_dict is None: parameter_dict = self.parameter_dict alt_work_dir = self._step.alt_work_dir alt_work_dir = jube2.util.util.substitution(alt_work_dir, parameter_dict) alt_work_dir = os.path.expandvars(os.path.expanduser(alt_work_dir)) alt_work_dir = os.path.join(self._benchmark.file_path_ref, alt_work_dir) return alt_work_dir else: return None def _run_operations(self, parameter, work_dir, pid=None): """Run all available operations""" continue_op = True continue_cycle = True doLog = jube2.step.DoLog(log_dir=os.path.dirname( self.work_dir), log_file=self.step._do_log_file, initial_env=self.env, cycle=self._cycle) for operation_number, operation in enumerate(self._step.operations): # Check if the operation is activated active = operation.active(parameter) if not active: self.operation_done(operation_number, True) # Do nothing, if the next operation is already finished. # Otherwise a removed async_file will result in a new # pending operation, if there are two async-operations in # a row elif not self.operation_done(operation_number + 1): # shared operation if operation.shared: # wait for all other workpackages and check if shared # operation already finished shared_done = False for workpackage in \ self._benchmark.workpackages[self._step.name]: # All workpackages must reach the same position in # the program if operation_number > 0: continue_op = continue_op and \ ((workpackage.operation_done( operation_number - 1) and (not workpackage.operation_done_but_pending( operation_number - 1)) ) or workpackage.done) and \ workpackage.cycle == self._cycle # Check if another workpackage already finalized # the operation, only if the operation was active # for this particular workpackage shared_done = shared_done or \ ((workpackage.operation_done( operation_number + 1) or workpackage.done ) and operation.active(workpackage.parameter_dict)) # If a workpackage is removed and restarted, a shared # operation will not be re-executed, user should be warned if shared_done and not self.operation_done( operation_number): LOGGER.warning( "\nShared operation in {0} was already executed". format(self._step.name)) # All older workpackages in tree must be done for step_name in self._step.get_depend_history( self._benchmark): for workpackage in self._benchmark.workpackages[ step_name]: continue_op = continue_op and workpackage.done if continue_op and not shared_done: # remove workpackage specific parameter shared_parameter = dict(parameter) for jube_parameter in self.get_jube_parameterset()\ .all_parameter_names: if jube_parameter in shared_parameter: del shared_parameter[jube_parameter] # work_dir = shared_dir shared_dir = \ self._step.shared_folder_path( self._benchmark.bench_dir, shared_parameter) LOGGER.debug("====== {0} - shared ======" .format(self._step.name)) continue_op, continue_cycle = operation.execute( parameter_dict=shared_parameter, work_dir=shared_dir, environment=self._env, only_check_pending=self.operation_done( operation_number), dolog=doLog) # update all workpackages for workpackage in self._benchmark.workpackages[ self._step.name]: # if the operation wasn't active in the shared # operation it must not be triggered to # restart if operation.active( workpackage.parameter_dict): if not workpackage.started: workpackage.create_workpackage_dir() workpackage.operation_done( operation_number, True) if continue_op and not continue_cycle: workpackage.done = True # requeue other workpackages if not workpackage.queued and continue_op: self._benchmark.work_stat.put( workpackage) LOGGER.debug("======================={0}" .format(len(self._step.name) * "=")) else: continue_op, continue_cycle = operation.execute( parameter_dict=parameter, work_dir=work_dir, environment=self._env, only_check_pending=self.operation_done( operation_number), pid=pid, dolog=doLog) self.operation_done(operation_number, True) if not continue_op or not continue_cycle: break return continue_op, continue_cycle def run(self, mode='s'): """Run step and use current parameter space mode: s = seriell (default); p = parallel """ proc_id = None # create individual log files for each processor in a parallel run if mode == "p": proc_id = mp.current_process()._identity[0] log_fname = jube2.log.LOGFILE_NAME.split('/')[-1] jube2.log.change_logfile_name(os.path.join( self.benchmark.bench_dir, log_fname.replace('.', '_{}.').format(proc_id) if (('_'+str(proc_id)) not in log_fname) else log_fname)) # Workpackage already done or error? if self.done or self.error: # the return value is only relevant for the parallel case, for now # for the serial case the return value is not used at all return {"id": self._id, "step_name": self._step.name} continue_op = True continue_cycle = True while (continue_cycle and continue_op): stepstr = ("{0} ( iter:{2} | id:{1} | parents:{3} | cycle:{4} | procs:{5} )" .format(self._step.name, self._id, self._iteration, ",".join([parent.step.name + "(" + str(parent.id) + ")" for parent in self._parents]), self._cycle, self._step.procs)) stepstr = "----- {0} -----".format(stepstr) LOGGER.debug(stepstr) # --- Check if this is the first run --- started_before = self.started # --- Create directory structure --- if not started_before: self.create_workpackage_dir() # --- Load environment of parent steps --- if not started_before: for parent in self._parents: if parent.step.export: self._env.update(parent.env) # --- Update JUBE parameter for new cycle --- if self._cycle > 0: self.parameterset.update_parameterset( self.get_jube_cycle_parameterset()) # --- Update cycle parameter --- update_parameter = \ self.parameterset.get_updatable_parameter( mode=jube2.parameter.CYCLE_MODE, keep_index=True) if len(update_parameter) > 0: fixed_parameterset = self.parameterset.copy() for parameter in update_parameter: fixed_parameterset.delete_parameter(parameter) change = True while change: change = False update_parameter.parameter_substitution( [fixed_parameterset]) if update_parameter.has_templates: update_parameter = list( update_parameter.expand_templates())[0] change = True update_parameter.parameter_substitution( [fixed_parameterset], final_sub=True) self.parameterset.update_parameterset(update_parameter) debugstr = " updated parameter:\n" debugstr += jube2.util.output.text_table( [("parameter", "value")] + sorted( [(par.name, par.value) for par in update_parameter]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) # --- Collect parameter for substitution --- parameter = self.parameter_dict if not started_before: # --- Collect export parameter --- self._env.update( dict([[par.name, par.value] for par in self._parameterset.export_parameter_dict.values()])) # --- Create shared folder connection --- if self._cycle == 0: self.create_shared_folder_link(parameter) # --- Create alternativ working dir --- alt_work_dir = self.alt_work_dir(parameter) if alt_work_dir is not None: # Check if given work directory contains any remaining variable if re.search(jube2.parameter.Parameter.parameter_regex, alt_work_dir): raise IOError(("Given work directory {0} contains a " + "unknown JUBE or environment variable.") .format(alt_work_dir)) LOGGER.debug(" switch to alternativ work dir: \"{0}\"" .format(alt_work_dir)) if not jube2.conf.DEBUG_MODE and \ not os.path.exists(alt_work_dir): try: os.makedirs(alt_work_dir) except FileExistsError: pass # Get group_id if available (given by JUBE_GROUP_NAME) group_id = jube2.util.util.check_and_get_group_id() if group_id is not None: os.chown(alt_work_dir, os.getuid(), group_id) os.chmod(alt_work_dir, os.stat(alt_work_dir).st_mode | stat.S_ISGID) # Print debug info if self._cycle == 0: debugstr = " available parameter:\n" debugstr += jube2.util.output.text_table( [("parameter", "value")] + sorted( [(name, par) for name, par in parameter.items()]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) # --- Copy files to working dir or create links --- if not started_before: # Filter for filesets in uses fileset_names = \ self._step.get_used_sets(self._benchmark.filesets, parameter) for name in fileset_names: self._benchmark.filesets[name].create( work_dir=self.work_dir, parameter_dict=parameter, alt_work_dir=alt_work_dir, environment=self._env, file_path_ref=self._benchmark.file_path_ref) work_dir = self.work_dir if alt_work_dir is not None: work_dir = alt_work_dir # --- File substitution --- if not started_before: # Filter for substitutionsets in uses substituteset_names = \ self._step.get_used_sets(self._benchmark.substitutesets, parameter) for name in substituteset_names: self._benchmark.substitutesets[name].substitute( parameter_dict=parameter, work_dir=work_dir) try: # Run all operations # continue_op = false means -> async operation or wait for # others in shared operation # continue_cycle = false -> loop cycle was interrupted continue_op, continue_cycle = \ self._run_operations(parameter, work_dir, pid=proc_id) # --- Check cycle limit --- if self._cycle + 1 >= self._step.cycles: continue_cycle = False if continue_op and continue_cycle: # --- Prepare additional cycle if needed --- self._cycle += 1 self._remove_operation_info_files() elif continue_op: # --- Write information file to mark end of work --- self.done = True except RuntimeError as e: self.set_error(True, str(e)) continue_cycle = False if jube2.conf.EXIT_ON_ERROR: raise(RuntimeError(str(e))) else: LOGGER.debug( "{0}\n{1}\n{2}".format(40 * "-", str(e), 40 * "-")) # Delete parameters, which contain a method being # a function of a class. This avoids excessive memory # usage when the data is sent back to the main process. # It happens here, that these parameters are static and # therefore not changed within this workpackage execution. if mode == 'p': parameterDeletionList = list() for p in self._parameterset.all_parameters: if(p.search_method(propertyString="eval_helper", recursiveProperty="based_on")): parameterDeletionList.append(p) for p in parameterDeletionList: self._parameterset.delete_parameter(p) parameterDeletionList = None return {"id": self._id, "step_name": self._step.name, "env": self._env, "cycle": self._cycle, "parameterset": self._parameterset} @staticmethod def reduce_workpackage_id_counter(): Workpackage.id_counter = Workpackage.id_counter - 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5629401 JUBE-2.6.2/platform/0000775000174700017470000000000014603772011016255 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/platform/lsf/0000775000174700017470000000000014603772011017041 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/lsf/platform.xml0000775000174700017470000000701614603772010021415 0ustar00gitlab-runnergitlab-runner bsub < submit.job mpirun ready error 1 1 1 $nodes * $taskspernode $threadspertask normal $jube_wp_envstr ALL job.out job.err 00:30:00 -x ${submit_script}.in --> ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/lsf/submit.job.in0000775000174700017470000000120214603772010021442 0ustar00gitlab-runnergitlab-runner#!/bin/bash -x #BSUB -J #BENCHNAME# #BSUB -o #STDOUTLOGFILE# #BSUB -e #STDERRLOGFILE# #BSUB -q #QUEUE# #BSUB -N #NOTIFY_EMAIL# #BSUB -n #TASKS# #BSUB -R "span[ptile=#NCPUS#]" #BSUB -W #TIME_LIMIT# #BSUB #EXCLUSIVE# #ADDITIONAL_JOB_CONFIG# #ENV# #PREPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #FLAG# ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/platform/moab/0000775000174700017470000000000014603772011017173 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/moab/chainJobs.sh0000775000174700017470000000052314603772010021431 0ustar00gitlab-runnergitlab-runner#!/usr/bin/env bash if [ $# -lt 2 ] then echo "$0: ERROR (MISSING ARGUMENTS)" exit 1 fi LOCKFILE=$1 shift SUBMITSCRIPT=$* if [ -f $LOCKFILE ] then DEPEND_JOBID=`head -1 $LOCKFILE` JOBID=`msub -l depend=afterany:${DEPEND_JOBID} $SUBMITSCRIPT` else JOBID=`msub $SUBMITSCRIPT` fi echo ${JOBID} > $LOCKFILE exit 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/moab/platform.xml0000664000174700017470000000737114603772010021550 0ustar00gitlab-runnergitlab-runner msub submit.job mpiexec -np $tasks --exports=$jube_wp_envlist ready error shared ${shared_folder}/jobid ./chainJobs.sh false 1 1 1 $nodes * $taskspernode // $threadspertask $threadspertask $jube_wp_envstr abe job.out job.err 00:30:00 ${submit_script}.in $chainjob_script ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/moab/submit.job.in0000664000174700017470000000123514603772010021577 0ustar00gitlab-runnergitlab-runner#!/bin/bash -x #MSUB -S /bin/bash #MSUB -N #BENCHNAME# #MSUB -M #NOTIFY_EMAIL# #MSUB -m #NOTIFY_MODE# #MSUB -l nodes=#NODES#:ppn=#NCPUS# #MSUB -v tpt=#NTHREADS# #MSUB -l walltime=#TIME_LIMIT# #MSUB -o #STDOUTLOGFILE# #MSUB -e #STDERRLOGFILE# #ADDITIONAL_JOB_CONFIG# #ENV# #PREPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #FLAG# ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/platform/pbs/0000775000174700017470000000000014603772011017041 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/pbs/chainJobs.sh0000775000174700017470000000052314603772010021277 0ustar00gitlab-runnergitlab-runner#!/usr/bin/env bash if [ $# -lt 2 ] then echo "$0: ERROR (MISSING ARGUMENTS)" exit 1 fi LOCKFILE=$1 shift SUBMITSCRIPT=$* if [ -f $LOCKFILE ] then DEPEND_JOBID=`head -1 $LOCKFILE` JOBID=`qsub -W depend=afterany:${DEPEND_JOBID} $SUBMITSCRIPT` else JOBID=`qsub $SUBMITSCRIPT` fi echo ${JOBID} > $LOCKFILE exit 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/pbs/platform.xml0000664000174700017470000000717614603772010021421 0ustar00gitlab-runnergitlab-runner qsub submit.job mpiexec -np $tasks --exports=$jube_wp_envlist ready error shared ${shared_folder}/jobid ./chainJobs.sh false 1 1 1 $nodes * $taskspernode // $threadspertask $threadspertask $jube_wp_envstr job.out job.err 00:30:00 ${submit_script}.in $chainjob_script ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/pbs/submit.job.in0000664000174700017470000000122414603772010021443 0ustar00gitlab-runnergitlab-runner#!/bin/bash -x #PBS -S /bin/bash #PBS -N #BENCHNAME# #PBS -M #NOTIFY_EMAIL# #PBS -l nodes=#NODES#:ppn=#NCPUS# #PBS -l cput=#TIME_LIMIT# #PBS -e #STDERRLOGFILE# #PBS -o #STDOUTLOGFILE# #ADDITIONAL_JOB_CONFIG# #ENDPBS cd ${PBS_O_WORKDIR} #ENV# #PREPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi cd ${PBS_O_WORKDIR} #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #FLAG# ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/platform/slurm/0000775000174700017470000000000014603772011017417 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/slurm/chainJobs.sh0000775000174700017470000000113214603772010021652 0ustar00gitlab-runnergitlab-runner#!/usr/bin/env bash if [ $# -lt 2 ] then echo "$0: ERROR (MISSING ARGUMENTS)" exit 1 fi LOCKFILE=$1 shift SUBMITSCRIPT=$* if [ -f $LOCKFILE ] then DEPEND_JOBID=`head -1 $LOCKFILE` echo "sbatch --dependency=afterany:${DEPEND_JOBID} $SUBMITSCRIPT" JOBID=`sbatch --dependency=afterany:${DEPEND_JOBID} $SUBMITSCRIPT` else echo "sbatch $SUBMITSCRIPT" JOBID=`sbatch $SUBMITSCRIPT` fi JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then exit $JUBE_ERR_CODE fi echo "RETURN: $JOBID" # the JOBID is the last field of the output line echo ${JOBID##* } > $LOCKFILE exit 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/slurm/platform.xml0000664000174700017470000001027714603772010021773 0ustar00gitlab-runnergitlab-runner sbatch submit.job ready error srun shared ${shared_folder}/jobid ./chainJobs.sh false ${jube_benchmark_name}_${jube_step_name}_${jube_wp_id} 1 1 1 $nodes * $taskspernode $threadspertask batch "#SBATCH --account=$account" if "$account" else "" NONE $jube_wp_envstr NONE job.out job.err 00:30:00 ${submit_script}.in $chainjob_script ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/platform/slurm/submit.job.in0000664000174700017470000000143014603772010022020 0ustar00gitlab-runnergitlab-runner#!/bin/bash -x #SBATCH --job-name=#BENCHNAME# #SBATCH --mail-user=#NOTIFY_EMAIL# #SBATCH --mail-type=#NOTIFICATION_TYPE# #SBATCH --nodes=#NODES# #SBATCH --ntasks=#TASKS# #SBATCH --cpus-per-task=#NTHREADS# #SBATCH --time=#TIME_LIMIT# #SBATCH --output=#STDOUTLOGFILE# #SBATCH --error=#STDERRLOGFILE# #SBATCH --partition=#QUEUE# #SBATCH --gres=#GRES# #ACCOUNT_CONFIG# #ADDITIONAL_JOB_CONFIG# #ENV# #PREPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then #FLAG_ERROR# exit $JUBE_ERR_CODE fi #FLAG# ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1712321544.5789402 JUBE-2.6.2/setup.cfg0000664000174700017470000000004614603772011016252 0ustar00gitlab-runnergitlab-runner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712321544.0 JUBE-2.6.2/setup.py0000664000174700017470000001253614603772010016151 0ustar00gitlab-runnergitlab-runner# JUBE Benchmarking Environment # Copyright (C) 2008-2024 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # For installation you can use: # # python setup.py install --user # # to install it into your .local folder. .local/bin must be inside your $PATH. # You can also change the folder by using --prefix instead of --user import os add_opt = dict() try: from setuptools import setup import sys add_opt["install_requires"] = ['pyyaml'] if sys.hexversion < 0x02070000: add_opt["install_requires"].append("argparse") except ImportError: from distutils.core import setup SHARE_PATH = "share/jube" def rel_path(directory, new_root=""): """Return list of tuples (directory, list of files) recursively from directory""" setup_dir = os.path.join(os.path.dirname(__file__)) cwd = os.getcwd() result = list() if setup_dir != "": os.chdir(setup_dir) for path_info in os.walk(directory): root = path_info[0] filenames = path_info[2] files = list() for filename in filenames: path = os.path.join(root, filename) if (os.path.isfile(path)) and (filename[0] != "."): files.append(path) if len(files) > 0: result.append((os.path.join(new_root, root), files)) if setup_dir != "": os.chdir(cwd) return result config = {'name': 'JUBE', 'description': 'JUBE Benchmarking Environment', 'author': 'Forschungszentrum Juelich GmbH', 'url': 'www.fz-juelich.de/ias/jsc/jube', 'download_url': 'www.fz-juelich.de/ias/jsc/jube', 'author_email': 'jube.jsc@fz-juelich.de', 'version': '2.6.2', 'packages': ['jube2','jube2.result_types','jube2.util'], 'package_data': {'jube2': ['help.txt']}, 'data_files': ([(os.path.join(SHARE_PATH, 'docu'), ['docs/JUBE.pdf']), (SHARE_PATH, ['AUTHORS','LICENSE','RELEASE_NOTES','CITATION.cff', 'CODE_OF_CONDUCT.md', 'CONTRIBUTING.md'])] + rel_path("examples", SHARE_PATH) + rel_path("contrib", SHARE_PATH) + rel_path("platform", SHARE_PATH)), 'scripts': ['bin/jube', 'bin/jube-autorun'], 'long_description': ( "Automating benchmarks is important for reproducibility and " "hence comparability which is the major intent when " "performing benchmarks. Furthermore managing different " "combinations of parameters is error-prone and often " "results in significant amounts work especially if the " "parameter space gets large.\n" "In order to alleviate these problems JUBE helps performing " "and analyzing benchmarks in a systematic way. It allows " "custom work flows to be able to adapt to new architectures.\n" "For each benchmark application the benchmark data is written " "out in a certain format that enables JUBE to deduct the " "desired information. This data can be parsed by automatic " "pre- and post-processing scripts that draw information, " "and store it more densely for manual interpretation.\n" "The JUBE benchmarking environment provides a script based " "framework to easily create benchmark sets, run those sets " "on different computer systems and evaluate the results. It " "is actively developed by the Juelich Supercomputing Centre " "of Forschungszentrum Juelich, Germany."), 'license': 'GPLv3', 'platforms': 'Linux', 'classifiers': [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: GNU General Public License v3 " + "(GPLv3)", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.2", "Topic :: System :: Monitoring", "Topic :: System :: Benchmark", "Topic :: Software Development :: Testing"], 'keywords': 'JUBE Benchmarking Environment'} config.update(add_opt) setup(**config) try: import ruamel.yaml except ImportError: print("Warning: The python package 'ruamel.yaml' is not installed. The validity of yaml files cannot be checked properly and silent errors can occur. Nevertheless, the installation is complete.")