././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3851166 JUBE-2.7.1/0000775000174700017470000000000014626040416014433 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/AUTHORS0000664000174700017470000000107314626040416015504 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=1717059854.0 JUBE-2.7.1/CITATION.cff0000664000174700017470000000161714626040416016332 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=1717059854.0 JUBE-2.7.1/CODE_OF_CONDUCT.md0000664000174700017470000001256614626040416017244 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=1717059854.0 JUBE-2.7.1/CONTRIBUTING.md0000664000174700017470000001554714626040416016700 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=1717059854.3731167 JUBE-2.7.1/JUBE.egg-info/0000775000174700017470000000000014626040416016652 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/JUBE.egg-info/PKG-INFO0000664000174700017470000000363414626040416017755 0ustar00gitlab-runnergitlab-runnerMetadata-Version: 2.1 Name: JUBE Version: 2.7.1 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=1717059854.0 JUBE-2.7.1/JUBE.egg-info/SOURCES.txt0000664000174700017470000000647614626040416020553 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 jube/__init__.py jube/analyser.py jube/benchmark.py jube/completion.py jube/conf.py jube/fileset.py jube/help.py jube/help.txt jube/info.py jube/jubeio.py jube/log.py jube/main.py jube/parameter.py jube/pattern.py jube/result.py jube/step.py jube/substitute.py jube/workpackage.py jube/result_types/__init__.py jube/result_types/database.py jube/result_types/genericresult.py jube/result_types/keyvaluesresult.py jube/result_types/syslog.py jube/result_types/table.py jube/util/__init__.py jube/util/output.py jube/util/util.py jube/util/version.py jube/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=1717059854.0 JUBE-2.7.1/JUBE.egg-info/dependency_links.txt0000664000174700017470000000000114626040416022720 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/JUBE.egg-info/requires.txt0000664000174700017470000000000714626040416021247 0ustar00gitlab-runnergitlab-runnerpyyaml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/JUBE.egg-info/top_level.txt0000664000174700017470000000000514626040416021377 0ustar00gitlab-runnergitlab-runnerjube ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/LICENSE0000664000174700017470000010451314626040416015444 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=1717059854.3851166 JUBE-2.7.1/PKG-INFO0000664000174700017470000000363414626040416015536 0ustar00gitlab-runnergitlab-runnerMetadata-Version: 2.1 Name: JUBE Version: 2.7.1 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=1717059854.0 JUBE-2.7.1/README.md0000664000174700017470000001026114626040416015712 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/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/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/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=1717059854.0 JUBE-2.7.1/RELEASE_NOTES0000664000174700017470000006577314626040416016430 0ustar00gitlab-runnergitlab-runnerRelease notes ************* Version 2.7.1 ============= Release: 2024-05-30 * Added: GitLab-CI tests for *make* commands. * Added: *jube continue* test for tagging example. * Fixed: Only in the case of *jube run*, check that there is a tag attribute description for tags that are not used in the input file. * Fixed: Glossar and *jube help*. Version 2.7.0 ============= Release: 2024-05-21 * Added: New ** tag, which includes the ** tag to write a description for each tag specified in the input file. * Added: New command *jube tag* to print out the tag description of a given input file or benchmark directory. * Added: A new operator *^* (exclusive disjunction (*xor*)) for the *tag* attribute and **. * Added: New *primekey* attribute for the *key*-tag of the *database* to define whether this *key* is a primary key of the database or not. Can be set to *true* or *false* (default: *false*). * Added: New environment variable *JUBE_VERBOSE* to set the verbosity level. * Added: Warning when the current verbosity level from the command line or environment variable is out of range. * Added: New environment variable *JUBE_BENCHMARK_OUTPATH* to set the benchmark outpath. * Added: New command line option *--outpath* of *jube run* to specify the benchmark outpath. * Changed: The *jube info* command now also shows information about all sets, steps, analyser and result from the *configuration.xml*. * Changed: The *outpath* attribute of the ** tag no longer needs to be specified in the input file if either the command line option *--outpath* is set or the new environment variable *JUBE_BENCHMARK_OUTPATH* is used. The command line option overrides the environment variable, which in turn overrides the attribute in the input file. * Changed: The modified convert type warning is only logged to the *analyse.log* file and no longer printed to the console output. * Changed: If the *title* attribute of a ** ** is set, then this title is used as the name of the database column instead of the parameter or pattern name. * Changed: The ** tag is now a subelement of the new ** tag. * Changed: Changed the name of the source directory from *jube2* to *jube* and adapted the code to the name change. The links to the documentation on the websites have also been adjusted accordingly. * Deprecated: The *primekeys* attribute of the ** is no longer supported. Use the new *primekey* attribute of the ** ** instead. * Deprecated: ** will no longer be supported on a global level. Instead, it should be specified in the ** tag. * Removed: Comma character (*,*) is no longer supported in ** and the *tag* attribute. Instead use *|*, *^* *+* and *!*. * Fixed: Allow YAML scripts to include a list of nested parametersets with *!include*. * Fixed: Allow YAML scripts to use the *tag* attribute in the ** of the **-tag. 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=1717059854.3731167 JUBE-2.7.1/bin/0000775000174700017470000000000014626040416015203 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/bin/jube0000775000174700017470000000200214626040416016050 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 jube.main if __name__ == "__main__": jube.main.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/bin/jube-autorun0000775000174700017470000000624014626040416017553 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=1717059854.3731167 JUBE-2.7.1/contrib/0000775000174700017470000000000014626040416016073 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3731167 JUBE-2.7.1/contrib/schema/0000775000174700017470000000000014626040416017333 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/contrib/schema/jube.dtd0000664000174700017470000002007014626040416020754 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/contrib/schema/jube.json0000664000174700017470000011122214626040416021152 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" }] }, "tags": { "description": "It is used to describe tags by name.", "anyOf": [{ "type": "array", "items": { "$ref": "#/$defs/tags_object" } }, { "$ref": "#/$defs/tags_object" }] }, "check_tags": { "description": "It specifies a combination of tags that must be set (logical operations: \"+\" (conjunction), \"|\" (disjunction), \"^\" (exclusive disjunction), \"!\" (negation))", "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "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 }, "tags_object": { "type": "object", "properties": { "forced": { "description": "If forced is set to true, you will be forced to describe each tag specified in the input file.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "tag": { "description": "Contains the description of the tag with the given name", "anyOf": [{ "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/tags_tag_object" }, { "type": "string" }] } }, { "$ref": "#/$defs/tags_tag_object" }, { "type": "string" }] }, "check_tags": { "description": "It specifies a combination of tags that must be set (logical operations: \"+\" (conjunction), \"|\" (disjunction), \"^\" (exclusive disjunction), \"!\" (negation))", "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "string" }] } }, "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), \"^\" (exclusive 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" ], "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" }, "primekey": { "description": "If primekey is set to true, the key is added to the database primekeys.", "type": ["string","boolean"], "pattern": "^(true|false)$" }, "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 }, "tags_tag_object": { "type": "object", "properties": { "_": { "description": "Description of the tag", "type": "string" }, "name": { "description": "The name of the tag", "type": "string" } }, "required": [ "name" ], "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=1717059854.0 JUBE-2.7.1/contrib/schema/jube.rnc0000664000174700017470000001761114626040416020772 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 | 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 } tags = element tags { attlist.tags, (tag | check_tags)* } attlist.tags &= attribute forced { "true" | "false" | "True" | "False" }? tag = element tag { attlist.tag, text } attlist.tag &= attribute name { 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 primekey { "true" | "false" | "True" | "False" }?, 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=1717059854.0 JUBE-2.7.1/contrib/schema/jube.xsd0000664000174700017470000004333614626040416021011 0ustar00gitlab-runnergitlab-runner ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3731167 JUBE-2.7.1/docs/0000775000174700017470000000000014626040416015363 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/docs/JUBE.pdf0000664000174700017470000152777014626040416016626 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 235 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 236 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 238 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 240 0 obj << /Length 318 /Filter /FlateDecode >> stream x}N0EY&R;x[XaUi+ g\Gќ;650eqZpa&d%9JsоiCU^㎰Q]MЖC8gBVP`Gb(lX u^ݮXVsr'\iVX:'@G%:ZFjt%X0 ܷݫߧrUX: 20,Y8:3LCi>|LSx٤,<_d%|D,e侭3&U `x endstream endobj 234 0 obj << /Type /XObject /Subtype /Form /FormType 1 /PTEX.FileName (./JUBE-Logo-eps-converted-to.pdf) /PTEX.PageNumber 1 /PTEX.InfoDict 245 0 R /BBox [0 0 267 113] /Resources << /ProcSet [ /PDF ] >> /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 248 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 293 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[ oMUl3'ٹnjuC<bZ Pvvnv댬bdCbmkrIQNQ>!N>묒NE0+($ȥy6%oƭ’1:{#UC_VO (n9T!,pbq8xXBBGsx?燛m@)`A>Hmlkt1;F endstream endobj 315 0 obj << /Length 514 /Filter /FlateDecode >> stream xMo0;9_]'z-5K`[ O屝 l"6`?1k>ͭdBCVSF&a 6VTK#Iܧw8&ƙD1йk^4^_pNRlW]>sJψcVe*~>ma<P8MW4mlnWBGR=]{f1^TŪԜgв̫PT)X[Di3?E?c}HЏ> 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 325 0 obj << /Length 217 /Filter /FlateDecode >> stream xڕN0EYR3";.@&)m*~7 $6,,\yc,#W+-7p\𨼂֯ t=>  m8,9Ht(.d'y Q\5vǡz U5A\<v3ݟZkpP#yjV͑:NQ qNcwi1#rfoO endstream endobj 331 0 obj << /Length 2905 /Filter /FlateDecode >> stream x[o6B] KS$EI]!KP$ KWvqܿf#n.hHCp8!zQxÓz2/x{ePʣ$wZ=K?G?}<:^Lbɤ̛X]p.UE02.WEvV[x")uΓMkZUy^|`\YӅd6Э4OE>tvaV;Kte&M]ϙ:,IcEDA "DqqX0m#j%>4]J-|RyZ,8Vo=vP:{iZ¤,6#(bL(3s8QR8A6_盶y!t|fb< AmlJ?"ipC|G`V|G(M yZ%(+ {%Yf*VSLցqӪ//qd]NL$bm]k]I Fqh5ݶI82__ JnY ~%1 pZ[`Θ-,ᐭѬ,N6W0s\ΞT{Rw#c̎q;&$0g҆YMH.M.h\Y@$x^ "DipgƱCk_~8OIY\:L,GmlwI>bYctJ8hS$'i@Z&DbcWU _7{D7,_SGt+IM%RWXZ{9/݀eR')_ca &<`" ĪLYmnͅ)M̞ϕ}z Oip2TЧLLZzf`=z!Ow! I?21]U 4]#ѣx)8~P[' N+Ʊ{[/0yi ` "s#jSAF=Z~ Z:}  䦢^"`>ځ`rDmT=UL;1s֟MN] 4"Q d{gǤd JA2SS}p+IǷiSۻ  /"b0k :6.ޚ:O%tbPXݠS5YWF --J:rϢ @'EAam\.b@i 3L$Bd0FDv*DTl ,S9b#R:-Tl4Hui(#T2(٧ lgHnGd"Vi4:!4\[oS n= 1F(g7a' rn* Ȉy( rE9>g(lT'j*;(Q"ׇ3d\'[, rg wJhjN^?նݜH/LHښn]Q * N:㮐H`>>VխC!:q+DW*׿J' \h@,Ll"+ f#pNShL(16@Cs /{nǻ;nݕj œ'{>я+ku*&autjbf>;ЧF,DJ0 ܾ%/##xQH''Gڭƒbź5WZJ1 sx ҉t!HlwEz'-S²#e]ǒAcGƄĭ%=') @C7kn &v ̉i%2O#nWXͣ܉;ܸKCGWGʫ<?0у*K8^P֬kN&B X áO0%L', Yۉ bln{q`8.V6&ڕYkKSCtg/Wv?[ ]٭O!avy#@2F ? ` }DD|<\ endstream endobj 204 0 obj << /Type /ObjStm /N 100 /First 881 /Length 2517 /Filter /FlateDecode >> stream xڵZQo7~ׯ[;Kr0 $uؽ2dH}CQbRZُ73$#LpbPd#Ǜ'QOZGI| gHZf mțe0A>d& dEoCQ/)".b|,g+dPiHINok+֏YHz* xRЂ=z𚃡Y;=[bd˳{w\L ЁHA#22Q*hb֯KXM, 9MQ*&%h UpMVgÆSYba)*.A5d;HačT _U3B79bm`ǀkO`9z}LNQ߰ #9_@+7WҌ3E в+` T6= F&dF:Z @EP%K2#cqw{GY2.]EVa0\饙.^-<6YO?&asJ/+l$]I۔vez{̷{e]gUi ӳ|iyW-VW[K|?lb㎌c%Ag^|FΦW.fz~n>~Y\+0Ӛjq<_U~=?ykTUtA" !+ϗקyjuu<\bS_OONVsɏ0w0tNO~pmWZc'˅ֳui-O.ObM=?7Fo7jGPQaZz5*NϿ0p`qӗ9l1,ygX5,71t^h`wUs]Jn2&Hel``*!)ȩeD2jw`xQ[4& SG% J@b5Al"  Yфic9 #7*C!Q&;F,<GüB2jEz(I4RF5sL%F^?]c`1M8hph5kǐi;E9},HGYz?.ɑ .ud!b]ϢHeМfrO#b"VDR-U8R1P $jZlmbQ0 \F)btϢ jd|xh!Z= xs3 ];5t'L|&`:j  Dn XYS b|ϥ^ޫ6`#\Q9c3 $<+,o16ƿ.etpWY@!3`:8Ō0Tp@4=mY[x"=m_e`[=K@,30@,"v==<2^#/\юS0u3.'TdyUc M'2QF4(FbFbW#iV5K ߰(Rzӷ<#==֍oz/CAn'8nR)]7VM1p?eݸ8=?w;]hsLll'$sAvEKCu]rZkY['ʟy6g.?}:^A}H kqq1XIz:[=ݠ.t\iy}a*ԃ4Aѭzwϣ/ѳp%yfng#=mKilgڊddW endstream endobj 344 0 obj << /Length 3483 /Filter /FlateDecode >> stream xZr6}W\ U0A$eO廪ԨE TB"u>͡w ,O/ Д =؁Y2-<ي178Zo7ƴmej/ڢ$@:Kq_}wb@__~8?D-^5,}_jx'f%nf]\:fh#l@#ߘg O%T̷ah1_*ml5U˖Me;!ĢbҠ5a Q3]xHR/v]UF]`ѮBؼ&#@cb"-6H\μ-rH5tKw&k֛s)澙Gj}oNNC8 BbxZĕR=7\gHQ>1 z]a *rdHq.qn`jI˕@ K }_"(GM0暀gلA:dIp֍@7@8IP!<9Hn:]&"^1t7)LX̭߿5bƮotcf@Zg7lJZMnۚk 26Чek𻚵83(;'omr;toej!Hz}FJȸը͘HׁA$NMUfs5w]ښ"Ck.5 Q9;. yf]MpJ" u8Oʵ(ՠc*Bj)I@m35FJ92t2'\° 5Q~֟Quf+ j(\`aEd`AHA4#.:onYKCAAM:pcwGf451&~uȁ&l[] Rtyi& }Rv3FeP))@Hux~a/a €9>J~79kPh_8Wp坐Iv&̈datpYKYg.EN}"I۝ƒÓSꘅRWWaDH ž~ak8\B(payҽT6 0oURj P~~p/߼;:n &(rxTNomxTmO)26 a$,@>3&7L/TK"6ˎ!3]&GEcTi6Q3T3]~44 qc))Kiz/@؈>R¨X a2&6p+ gڙ8*9 nqD q,:6AMOTJ+ Ŧ&Bs^թZ8Z^eWTMlSd)D4;foa 6w ԣ*SC]w JmAlJm~ ?rv?f\Z_OMVMIB+h>hɁnCR,X84\^ТZAEE ƌĤW蝦Ղi:T C E`߇jKX[ݧ:4]I0KVMQ]ZzlB?!YObOMSWCWLumrenh ty6rٻħoLt.[BX pyD`|#:y۲۠ߏX<\PrEp⍡b{*2!>rg#ɑ%ƘmR&V,\LPmK G^>ﰂTn\2t=mޙ=Pi׽6u/j.I6M~6qZ;1YU#.ϕC| CaޙuwLHBxzy CUԬX=c nH]ZhB.ug,/Q SC_"EӜ#;`c4x unG\%`VW-+PqHA~KOP?k[+gLRٟv֢Qbp}תD,dkeJU@KX`hx/, @'re,TaE@Z\s+ZA pXD~5:}vx^P0~HMYV_~<{j:1f[9}P3]N]!٥0O?zAKmaܭvXPL;h9 )NjN!^H>ʏlŷ6OV[|/1@Ln~wGNF߇$웺/ſJ}05ѣ53k[sv![rnXL6;j|I>뚾 endstream endobj 351 0 obj << /Length 3568 /Filter /FlateDecode >> stream x\mBI y⫤z@-ҤEvW=[r,9EBZ׋Ù(rf8|8wqtї'B(c&:Lh%Yd*e>t.tz[U{:׳-/VE X/ߜ|~~ G^Y-'?GK&ƶZGJs&UNb'jG ˒J_E sE]e+ gzMͶ8z[YW_aM (^E^}%imRftYѯ4*VbIg )SB|R:so8JQF0ǣgv |}3ǟ/{ߏ5 X$S#aFjR}{&Ft ^KXS_d?s~'M(ԟ8ʼ 4m9&VHngcE߾x{c7m\C-ݚˣ=$(p\{Ԃ5?ey=yaЃ2O3,X|bYTC+O$=xҲ>#!rM8RQqQy|$$_`xmC/PuQTu}wOE'OuSnX&Gѿw1q,s~]ISViϱT+ a }ԾPk^|?@تO$% w$uZuH OQhј]J3{hPh= :]ܗ 5shw6dbJIi'#(ܣj x3Fc!)(ޏ]:bp9DOSojJCYNh nܣ!S+vbxpvQw閚b}QoO|>䞟9-`nPW܄R& -yr4/^ѥi}LӴUT씚?jRbxQۮ`LWwv!}|Qqя xZ=<T ) SɨP ꝈLMLhá|H`0Lĕo y|[,j` {UQXsy<Vo}gϕ8 ě `aG$}۩sniRzBNF o}7Q=*Oe_5Qm*嗭 ڷ׮1]IWrH̀K2().,_(|*+dǼ)!ۺv y34= ,%벡0vT1`xgǶlzG G S0QͽJİ w!H9"6M'ƪ-mʆOFP9FzXBLPb?"13ia۫!n_#^5b\j6Ţ.\V*kwk4s(b}~8"&`(K&׉ \Y4l;+=meT>b7pX3!aWTh«r ~yyKO4Lh X0RQ> #/ ;kܸUCϨYЪ^w2٩ֽ@PZCHK0>.ƹp۱ Y:Ϯn[,ZQ*p HYRݲh4^yaԨc/0+XPKrQߛ)ݒ5v;d; MZ'ju[7MG]Rũޱd`7 KeiO+ wmoz-$Ǽʲ_P"ĄBbXE_A﹅;7h2PmA e֛my՞u+a{/v bmїZ%%_tMkňv-FN5pND'S8nQJmv벅 \v?46x:@{p7጑V䦞 G@sZYSd${rIG6]~NC]Βh>h=sdj~{S_EWvW6-]`K2'8yǏx3I ?3#tD.ȥ{q8n8G?vP5whtt֘:a/%E H?yH/ÅCHȔMxւCҿ9t୉n;+=$MJkSFnɵ.аe];:Ƃ&maoJnHOGq)@pr4XG x; gFcs] >Ç,|Z-Z 4j'zjaŽC{٥8)߷Co!SL.Hl2S |\%L1V=3ˆO[tKƤMHMqC\g!Jka*e;}+"iaJfr_X});kKBbCa1这އGIX8^N?QjH}}l4dJ(FUBiD;cc$*||Q&LNepRcm]Q"khR"~!{ Ş{Bv` 'k!,XX]> stream xZmo6_"ne& ]k>9C]٫VHںƥCr,;nV"E<3$͌DO^WMs]p=ёha=gg$ ܼR vmوDi9r8 x#K`:όEy&42!%S& G!)ΰOb8C"A#r<1F, B# cҰ0ZѿVxZSxéB.ge^Bp%uCdcX Yz6K?&&}BJ׽. Nd.I`C;߸G{+﫺M@)U32U# ddxBJ(F V P5k5aIq%E9V)c<"ZIg=IlD[LV,%^rb]܋]Ĵv̭`Checwvp6Pq[r cM;K)tD>Uh Bj )N8&_Zt(V/vqG/YtOv3g8 !d">؝㓛ɣkSIsP@wqvuPwE싄I:#gIr]miō'O5 aF!#{/""V<;<k 5] E%w,lC~e? Oiu-u/a=_d,L )P/r볡U|Lܰ#EgwFj7HE{BiOWcoYh*;>=rCGVw*l{gڐ-%)$2A0$!9& 8e?yt/:8+gY,aZst9&й\]+sEg |*1NyYVnS^C'w}_$3<2Nkx,)ˢA\џr4'$H)dtʛl F!N|C5]|a];p`x4ͰWSth7n<&xm}o+Z?:mQ r]S9\esԀ>I.fhJ?G]/.B[O%D~jUr~KytRU3 o)z|7_[%O{ϟ>f0[CI˱'f͵81%A~R]f\-m^&!0aAb3!mhW yopmIV\/,7@8P^lW^ET}sm몍Љ`_Bi)`;*W/1P]7F-&lF: \Vs} b0׎t@Xe<dR,^ #_gv'7C^:zcKNPYS~\28N9V"\nRbe#Y#…NwzYVW =!ݢ$|W Emq&XzXh 6W]}YWuMɠ^Ⱦ-07nd/*5$ƆM>.3٫֘)=o+ sdƟZܵ5!f,5^d c[\6ٟ-,?ו0l.LՀ_;MXjLYyzq(&u1ͤ seGGW̙Xl, F˺6|j3[5.@ <1T'Cb,:@dDG+6duQ!o9Fu}4#M=ꈜ_@~'?yb ̴6mͿܧ˕Q Pۨ(8i endstream endobj 362 0 obj << /Length 2962 /Filter /FlateDecode >> stream xko~|9fKuj1:#;);|eKgr[ȝ۝.ͣeģO|QR+mtv)4*riTEfo_%T:/we*&|guN o^8;y"`*II]4_yˣ_F4}u`Yx_EO~>T> G $YK YΌpD/yUr/,gjMom~*쪨udK0(oeBFvӫk0+TdSy>)J\s6jב °Y$^$LKmM~Li%{{-k{QoO8i/#ujPiM-<zcVjyH޲AhXȿ+Ie M)Wp ;Q5H7-ਵQ-,p|?İiAU 8gf_ھdw2'EQ%l| i kf%6J1%8} :+oj,V3ǿPQ!/cV+D0V1TKc&kxyڮ D-wVSW "*QmW))pO,Ov*,[oVy|; aO{*4+: \(V`Y@W=!{O (7A}+B`ve͒Uƪ(γH y}9 [$ȗ0TRǷ@5xHBn>mZ@xw?b9S|A ejYf HE#?T@ ]E)qyw22$ ͳg¾F = lAm\W (Y|Fj*wsՔ˃ȉy b %f?? e{Mv=1oѰk74׋"E0]hqV3zQA3\idҧrˠb%Ő 3Mr0̦-毧JIۯ &-Xҕf?)ASD-6D>$zUjXT 7j<VIҾqe{XuYCg*J~JAiv'tMe6?/m1jxaSM[g<no&Jy=:jDI$ Rˌl괷UU ~Irf:SbVLQc87/Vk_|+VDD ۣ+ߟBt'#hLן-l.lI':<)OOㅡy f}|v:.zV/g>Wx)z4 R)gऔ)Ӿ[SB+͉q?dtO86#z|!3ôj:PŞ26"xT8(zXw'ӕ'wtwpw>tHGRxOt5KcE",_]MkPuY"kMvtCWjC2n CZx<k;]#o+t@Iuh$}ج'\ E45-q:o߿e侀K!Sx Ҿ3Ê@~Dw rVLWz=U9 iKॿhVl!.|v"NG&)\NM'11Y@l)7͸B 3xI4jwAuz6E PiV~nٹAXfa/\?y5B@5k8.p4qiL)Ԅɔi1:x=Н6we[uQc4 &ۆX0  &<۵[q ]58%yM}^(F&[g첃ĸ-Ҝ^f[uR'9=/i$xκ&WE "^]o.T3*.g^ØdtU у@Z5aV_ Ta> ̫f]MU.h1#H<Ȏt8vyQOaTf UQ8irĀ `XrA!ՎM)WON#1 JwH[alPxٟ1F9+{hw+tхaPxLZHM{)((GS>e&)w{5U,QO"WF3i+h !Y0FZöQqc~yP endstream endobj 367 0 obj << /Length 2916 /Filter /FlateDecode >> stream x\[s۸~`}g"w;Mڼzg,:N{. %ʦ63H>B8pobK* AGE,8G1 џiLVX&E^"䉹(DΏ>Ĵʑ`< 3(`(ѵP"GؓpKC#<( ,^YrY^_ksf"BGI̪B5o%ktK" AZ7'qU[  [ද>S:4(#$ BFeDi1@Xʱֺ.f+Q((Ҫ"Br8h|C6 X0m~-P?+ 4ƍ&Id})nUế)0]}ئ# ~bA}sVa$D7DP+2N[(RX-;͸fHTF+M}(CJUAYp,c |*ג4Vz4xٙ$t(^ICXP1gy}v4}\&6t)%Y%*XAL-ksf|aѴFB.(2T+r Q*(1]+SGY1SuotW%7ZUփ1 1 xd]bnigC1B* #*y1I* ?3pfjowb<)3 l ד4S]%ѐ>)+$yHΘ, >nkm1*g򍹳cьm7]33OD͓*Re[4FQnp,%dj]bB\8FB=(RC9[lP3DM+|DYR`֐5(DՆ(f!GӃq;;Ue^+Ns92i>\Ysg"h(~kԄ,Uف@AhC(@74! |O$-`HBuABun4H!VkE󖶠0BXxzZgĠG*:H!"n1h660Uˍ=}@O+>T A{*)C#~ h*d;-lOI?f4nܭ۔7 62@q?8NE HHLhE40M7s+FT9^\& ׄ{v7kRya-ubBUL6@뺧pOk;쒭I^oxNVVIW‚M !F ./ 'JOE'íu%Y,WfŬ1ba 2庈:$~2O2Ӄ$a I.m":0'HC%F!|BqkٰQPox rJ'ՉznuCbbfu5\U-WU4/k<>Nݜֶ00XzMQOY͊o71Gr k*m+4oK.nFG- "_;hP>ոv=a{D1kXbzq)[XٯnZVqhYB"a,JooLq&\n4G>DҲ`MZGmZ-KpO/Z e@CwҲEFw*/ 0T.{DZa*|Zt_A8)GEuFG "k" $moh'}@>̬J n6-e"J2XT]XAH#_'݉s ^[%n9t[5KHfڑ[ZzdL[ xnUR{E)b؅-ņ=qLx yqqvfg6h=wۺ'aNiJ>WQ2eTM:ya Y@>r;s垉鱆;:-$h!]G 6Nj.Ey?wb܉؉ <6>E߇Y =vס'psw:xzğ#מ)hCHI۷RѷEpD^?S2"Bhc-!18[G)(R3EY(ethKSr endstream endobj 373 0 obj << /Length 2876 /Filter /FlateDecode >> stream xks6i&B&iI۴srv2E[J"Ru< O+}0],%wQo.^|/Ps]]{B •0 "{7z7oxLrczӪ{P[?*T$|4Za5?T%lCͥoXx--׶ªiIJBTh$oûJ[8Wlz':$h kYJ8As,&x|!cA3(< V ĤGN0Id @zENlvPvY-g(!*MB2k@k+9@BE2Q82zEUE|$Ena+0Ӻ57u',%oR7;mqza %fybC˯a03v㹯*@a"qAs֨BۊAHˆifUaٶƚAZۗӐ") t res? M\ @\JScbqȦ{,B\dqn%| sU8 ִgk< T'`U`3m exFSӁXFLk4/&1#Y5_^2B/] b\}? .~2'ugNa t4ūQ)bA߁Wp\]q %,BWպk׷{Z Q˶ `Nۦ\E8j߯7_CÚ  s$B7QNNKmm30^̙[*e;掶;|Ð j;VB4P F:[۠l{|1Xm]7;Cd#(y}V$9M/tjğly%938IEl/;6i>#LU \xxnu;ys<ĞXl0Yck]5LӋmaM}OadY%~˫Um˥Pqϲp]xN{djt{t=R3ԛL Og $W2 vM|h[IFϢ@SuͪfEѹ:-lU[j]< aze$2$IgAY0 M$ {o,ot@qT77hX*O8F56pE,%+Ȏ:~_e$l)^k7:?  wp.TJo>씿7d1L*g3+*e6j[]MnؐxZ.,]*6%p )c1aP_gZntJ7gB*ݧU|(o='DUH'CLb[d]CP1B`~"C9|=W#!U8PgtUzpLӕųnyЯY{ meL|9$@'=B)ƙDİ/ W ؈ Oo!cwѿRI Mmw=.p _LP_j&G!>[c]І {luÎN87'f[Ǵ6> }XfS;Ԣ2E<+lSOn9fBY݉ `Օ$o_eƚVd<2|'e^ء7 endstream endobj 380 0 obj << /Length 3202 /Filter /FlateDecode >> stream xْ}Aaqt(۱]qڢ$cEʳڏO74uy-hEH4zo^|'dTsE14!"gΛџM_DU 1 $;ݏa'*/Zq Y$*iT{e);[l-_U>A)ǠѬ-z?A-M^~=H2YTuv1MV5@^~ޝef_rj֗0ܪ,Y\f|p!%gR(3uѬ D/~7+}y6VAt QH߬MV{sc?51k!s p+N;6`;_T\AEOh 8QgxTJ=2cFoƌjacAG=ڄkQ (w, BކM7VIu{-X ܸ-Ts.O,_6 ' ϙm*HLBjps0sMR_a2C 0/n).pؓg$.^5%Z(s?cBܘd~ͣ\YMƍU.D OZ{{rтcCZb5{d VOVasV7hSAES Ƚc Y%~\f BKYuB;|[ovGAee"]ɴhXᇶJ_H8Tϭ-)eW|bamX% ָ`DwvHߋ.і5}hXIk!%ʄ0ن{JҺ ƀaVSu`Jdcq ևwElx&3u]l{]?- tbST9dD@1M[6F`t9W7yΧDž$}D6PO߫3\bn*%\CWՂ(4zs-3VX?T@yh%*}3A!QjNLi;_jJ_j}T$QN!D|(3(fZyjnpu9~R%U]L]s5-],`NWY:ڠk5"ЖBm2!@0N a!.=?Nd| ;>Ʋ rvڬc/+Xlتd? G/ڰx~o1+1&_P^u}mf7>&OE9r^$}ֿ X,4Jx8ʁzkej)֧&RtmkaNe.j)Hp:]Wmya 1qN_^_/Vm=pKi+O"bA~xoȄ&zaxKT%/yɟ̒i4K?596%ϰ m~i ~t 1p~jyS^BVQ;Sza>ˮu!GH"E5$K} fC /4~Ce%gө_ {`uI!$^C[Iߜyo> stream xr]_Jy&zXznl48KWĆ$h^l+{^pY$!c=%{s߳m'/.Ob-Fx~x>Hm2LYbID_9yyyT8 VAot=&(d\De瓟N{ @"%1 sJ|#A7,p/ELӑLs*:bz[ݓ0 y>-"TpT࡟޾xgX H)x!G&8J7 Ed(PqCB_bR zC8 ƥ;fv`3e& ~ Jh뜄Am TrhHR. ϑpmCcH36 d B 9Rpp=`5&LO]$)M8ą}g eIEp D"1Nÿ=)NCDfi/K/g?/ߞ]Wf'y30/Ft(^b") qN'ϼ+B+/xQ}_{G|7lQoHir|¸5sQ.PDR(JAyR$Db̒P_taTm"re=aT^$6hGX<1n~JЕ}g!; 8VCl#2"^bOȧn{ͅ@~+m% l$U$ƷeMT ÉnP_$7 ɬ)'j$C91#^ LP (b?'Tt:N&#t쀑9;h9YJmKH K|Oe#k\Fd8t4]>k>+k%&2XqnO_a>))& Nj/`@ ]bS94Ìbg@9x4tCS*a|POH gY$Tg^P*X' -^vO}:\ٴE.>zL0;r&p /)tQǣi))vɫWKyv(ȳK$\sڍ4VnuHU4rrk8k?(2wqd7b\ /`!"Rn"||ɽ=ܣ3ex4 UC+^9TgM7ۅK~KDX5~ZG@)%>ӮVUJLyEI*zAjl;UղnneOc)(T-qL*/=uA4 VՠW~}jF4dz]Fpg~W8[Ł Bνje~gQyF4Bs@=ݴg,joZ{~x8L̟$}%hiM客HU}lLRuOt8BEyz#=,x$~i6NE{gD.] Lizg`Ԯ iқظso>%X4Iͱ2DZ"T{nE[B jZ 1 tbYe/?Й๛7My=j.u:lKgfq3@%yQ9O hɖ?1&Er!՟E+td+_pk - endstream endobj 389 0 obj << /Length 2685 /Filter /FlateDecode >> stream xZ[۶~_y&+AzةfgиKQS"Ru'? HJJ餞>,8F^]xQJҘ* mضUMf.2MT|;3kCOmen4c-\@jY"H*DFM(g|q4V>O\RLA%?%Džs''FU]? ҦYT6Gnv2;+6eUjurW(R[(Tt^wvuMAڠUv 'R)jj]6>3"vOI̼< e~&^׏y翄!ZY_ :XLϺ l+!"!s4ŔpC@\q Z(jf}U.x":l]˦yķi/l`ͷD8vm!~1bv&p |H~H%;i &Ktma|i-;*3vK<!Cf`C"ts֎d$r$gU@ݺ=Bmʧ Y: 悢ϯwtB aRͦG brB|@%&|]с3sfL~hUvUx=%xq]bԥ۶T1R$nHRuȃaWَ=غ*NDeq2#Iß-ds&Mt,S- \M*돖dq1jo ( h^I |9-7E)Ja嬤U lEqTUL)+aZ3!LxOv^7_G]D:IśTp'U@` a@D!r aTbBBfc9zU0DGDhz~{Pc*1RV&$ȶ&1ڈ-^jo:މu!Q !OD.F nȹOV#(1 x25$iss 5ծ+AX(ÐDJ0E 7IZq]~*j:wCќ޿$'I};‹um?n<cLHzV^;ݺL 08p.l#d뢵=['zl¢bu p /w+Lip} >WvSMpsor $+^qS&HOEak5^u ɇ4b+FCHĀL@m/Z>AȫCĽR3)J=+m Ro<my+% endstream endobj 399 0 obj << /Length 2524 /Filter /FlateDecode >> stream x\Ks6W0=[#oyTͤjʬsIEK]tD*mƣF"D]9!ϕy.:čAј&H |Bh )FF RC4#G,s@IyŅ&EIGʽp;ՕAׅ53Y4&@02)=#ROR@ ϵ+I.;!w\.I `Nft :3 rͲKˣoaM4<ɓ%V={iف2Ebk]foVE%uV 7s$ŅnV^UDβb6^HS0G?FS¹eޠB $hCȏ7UH,3~*oJHDHry1@RqU[ge# 6e$18Q9?"xUN9von'Le$NwB[% a!(J\"/qCD_#ʨ6`bɘ]\FD0*J5?բv-dj+ϖN[c)F 7z\3b5-#m6(=o+1%%xsΗ#2<0h xFXƠki J>'eӎa̸ ;c^capS/ 7cW,{sqr}W&sVtRC\x:%"4 yczT?9sG\k6ؐ>4PQ%uAX tߴ$T-KzI!xQs =k9 ^uMfjxgU!L"V߶n<0·,_# çtBۂljpW{ JO8rvPX&X%rL䴅rɓeqyK~厸|^p iy2-Jʡlȩ>lpL0jEo5FKv|򁖷)E[T V~or +Ol @Cm/kv%=k5g[~W2 :wc"X[0_0Rtғ뼫<>4 }o;/#iޯ6Z2.**hYa1P61սHcA&OL!:UG wYUwE ?O]Q%v n+ czg>4uh~\iȗ3g}Sj⍤FVfSޙ^O ߋl4n0ῳ>f[뼪÷+e#U2C+dqaNB4%2]'$2=mX#`-܅%`9B[;^]v7$E&Ga5ցZv cTŋ)i)O36~1> =AՖRHn> stream x\[o6~ϯВR1h@6,^(7R[^ ;(YedD\sE {ޜK"0!qFKZ#c K?0s鄽CM{ꁨ/o e֡?'.T`PHRR`3LCK6v[YlGjs |9 C(쯍4y ,,@0 ){t"9ܬ8x57YĬӝL;UrI-vahf+8$lg_K ;ZM[p0.p~AM:XPG!4@Nz2ȥR9{5og薆P3ТBG7pNDiT %䖂\*BVbHʗw0fr-dmʍ-+mQfa{Nwq3I+MŠD/:h[V?[;@2 o:R}^7(8W`m1\9)|x i*q(edSH$=sOEu9= FQ/C&Im,*,: ~si.|l^`BD̂v)Z9Eq\Q2n-l_[ ^ ~1Z={w0(V6)wyC8W:I=7v<4؏f}32;/nq,vgUj!Kw7.]t,lZk *8SKC[X9xؕ.Fށ?V=l(sosAV s[|=<!> stream x\s6_1U7xhR^urDy9'f(rBrUm~9Y}o~0F1X|GOQzQDE˅JT'jyr8,^.g8˗oׇ] gdYu<mUU(ٯ=z~G bUGI-ֻG/ *kkЉ,vӣ &UH,*2!d@^dɿDfC]u'?sKy-|Fi#'XLԫuUyJH"JaY [ ǵnzP̌4, ;tG:)tR)A-A-o[ ϟC j `u I_ܛy EV"(uf֐S[i\r&?DNŮ"DA@a]]/)ɻ.Hl rG-0CĀE:4 C ٷ0RScؙ8?79.a{VIhD7Fviv{5BIت1+s^ oEXtw2jn U6͌N5S̏J3~XLHVM5F=e+i$.PBps_7W{"}1^_ fϒHv/̐Bi?T2'w{}8^LY-UG0GskR&*42JL&WrWgpt/!vdž$Ę*:[X/(05X\TFfN3v3jSU6rM58;ڹ !+px]-̅Behni!I`}H:3P6gR OXjtΛ lh-]{?FˮB.hpDW_5ndQFq""Y0bx\EA"IE5PemLsJ|E~g {} O`uf-;gdB*ehz _r?gA!{cźq|+[ X $QPaI3,Gg gwECCT4 3~Jh~+sq\dq LjW!#ʂ=emupȲ̗N#m>[dtb EAgh#}3ʅtȳ;N~/~Jo>K{$ ﵶ01_#m[yNzL]Scq+y_|zg~6B+!RME*,Ʃ/.8P. a^蛥H4Г6(\m۾/Yx ӌ]AhAT317&Fl `,^lٌNVr[(b.U65ްq ~g2 [n&2rs[B P U3#!{ TpL6wV x!:b aT*bvu{n| GR`0\6mS՛:I`\Nυ)7}1`DFs9LtҧHr5VJ gVYruP WMb O`yvQIЪ>h"p p*+},Z93F&f%Adv&;e9j#, 6ұ, V CJ׍ :2#rb#<,0/Q7p-Sy%򮹔>l ؛LΪ}Չp9yʫqlvBK#ؤلpm^k[۷X^d$F(IzZL˭1xL%垔!`I=O)cf(6ax&@' b8U! $s3ŕn 32w641ˈN7a(ty,K9:|{!bK'GY`ɳ\A+D+!?e0ˎ0TAŇJhJ;5<@nG=aw'+ 01LoԻ1A'Zۇ}|Х$^ڟZ`<ɒɫ5SNpN'st8KAA!RauOu`CS d^ bͰ_[lQ^=[9Ӕ1o&wv9`^e$$";NjF~/F' “ZX-U)]8xW*-#7Tc3964_ssa| >1)) FG!~➁[4Y|p81(S4c8`.NcQŮ#%o;wJ8{j~j?yڷƗ a]BEBƇV)z7Pm|sC5nϐ> stream xYKFϯ#1~΁!8\y=6 R~|x!@ sw*&Ճ/ h AD"/g>x(YϫvQauu/2Ov8[|ztzb0 *J ۯ^JDkH4ՋhIgZ*jyqRb#.sax6bƢ `>.aDӸS,HaȨr@70K]*Ϯɷa[[Jݵv؎yӕX_gj!ϊWTK5SnYO鵗[4NH9Oj7S֢FGm,؀r\Q>(qg!(K>]*lU 9.AA6^s~jPt^6ҮSp5`BFrD_CۡP)c0Ws(a\x !tL4Vp@o8Dx\mpJHGqlNT1HjI8D U >"  Q?3'*d$Rk7ٓ2wJ?cO<:̗AG7Aڲ 8P0I6|.,Ep/\r}k,Y$ ,Ӌzmk=pn\3ӛpB>h%} XR2hBsw[)$R7\nm{BzsH )_6nItWl~}cq'X. 6mܝ匎SCIj|l5T 9UCNՐs5-P:;y~ZK:᭹:+ܶ>;jUOoBMr{beDpk&O,Fgp" lc+= etK Gjdt ,6_Z)|kMQe A%,SsXFEƠ+. kB Pҁ`3ݛcW "qCQyBff2yg1< q=~a;=8~w=%yF7X4Bꡜ VrM}cߦGiR8+֋p-« J˦c75T VჺY! aʬzZ4 N[{wEݢ2 4{)w㼉D[R$>/B>#-~*Vp@əٟ}xZ/h+ eA,s:XTpHg@d-/|UU\q$ld !Snd>9*d5iCN[7U_K_#u>u)zU uF>#=}<&s;4TC&p-~ ✛i8 Kze2bxցСt endstream endobj 424 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 428 0 obj << /Length 2547 /Filter /FlateDecode >> stream x\[s۶~q`u,wĝi2'=}J2#H*Qif1N܏/yVB u!֛WFD0E#ֆjsf< (Vt) wbF mB UgnLI^aΪ1)Wzp=׋i\=.AZu5WZJU j*w*N`mEI!Uc.@I)Äf(SYRCcb`hlLRmizxr0D(I\\\x3KPAGv(|N&)8͞Ç,ϗ; hYπcl֟t\t|xR4>w?l~8'3T`?># A?¾AaL~Հ0.bznERR1|D\"rElg鬨Œκt! Da'VnnXJeԒ3 t6ST]'2IE:J H1G2J>IOF 7W5#ntSK(1h uM36_AT3 y/%EhUS' PK$m(BpĴN U%Y4zET2) Z38^*8pcRXvX+tML%B`K{& ؔ믤NheC2/n>+'%LK%+^tfctR#LLl& C:  ۷Oc#m걐,[[nq< Sוdq>w˗Cu]0ȓtRCr.<Ggx?,NL~JnN?uԃ;Prw O;,nC{V Zj*Low_?% <]ZBZ1q?įtk*=(r|Xe38~cZ|jGaRM␚R7tx>FARUݳ'^eRV= ^P0k4` ڌʞQ D~ʳr*]ZJT-#ID8g!Crwlwvr6|QZq: M/|0Hm&[V: @1Aq;Idn%#Af=7OszB Tk9Cm& v#Dݞ5o5t„65kG@urnEu4ԣM|m1γQ!UjÔc tkc*BaE)#O3sH(}m{V#Jߑoѿ/'VD|Ҧ҉~܇ |'滚.'#* 7<.5 nEWXN0:Y~i>RGI칮yW/Y~ۙu|ݪltbiX[g}PA>k̬mQds0DJ/5ۺ~df.m]9̒tWɀ-dvH_:eg_fp3WPi爔#8z[>[gUu]קyTy bWUT߹c'ŊȰ;GbL־9bѢG{_1mI75 endstream endobj 434 0 obj << /Length 2762 /Filter /FlateDecode >> stream xkoF)P6&Y CD[R*JM"3˧iEй ryԻ'Bz! 5' \ "]μW>i<~0W>,l=QN_imp6~s˳LE=FD7]zC{0ޙ^ O*F>~9:Ti e$)C%瀾|MbE5Utg$3@K*_ػ*s5#7D75y5-a\^W7]^>EKu@4" nt M|aT\jlI')x iŅ$^;a~݋B͔v 9}@7Бͥo,"µ- !RZID2he̥T{I Q ^jZX%>T[ƜPl>|BL4S.Ue6+k"}BqrʏصCgpM\ Bi۷#BWclJh_ ;גּ8bBy1Жݳ $Jm .Lk~fW5bJn?W˿dl`|oO I */ٗBzg|oħ-M#RW,~o݃59_d^T6$m bh!F\6z2Buߎ2'>^!$ɆDozij9U2o[Lc_8# j/)^10wzؤ[[0qot^yt:qB R*Kpbe. 4G%]}SVm`u xS5֎cm)L^g?a0Pza!z' ]6v@e2[MݽF!P1)bޔY2 uj \YffYzSs*R{P*2*@m;%·,*),)n~%/zVOy6x1Hu[[Ӟ[Lw)cDS1C MgH[4b Ctou_H|ܹʳ̞[[w- 5z?ft-il"v7cIGfe[5"^C}(\2^Zf)p!~v_KĞ!kT6%aa^#sHp6ϓ阩5ulZF٬<KVJ5y2jΟaiȠ:@ڈQ@qohXBVlS<ɦf"O|ɜt`IѰ)(AᥘZAK9X>Gڣd12mԢ6=* i%:v;΁ <[nջkbj);gG on̪w֟@š%@i 0&.EXS0Va&厦 u"2te[[부j: k?BfeX:o:Xaoˍ#RE^O"9d$oy beE40&Hh?Sw!*(gx2zPYO[# 6͗NN~Bn䐼+P5P]O ǝ_ Gܛ![a,BC"_A:gbi?߸dQߦ]T=r{X0kWM]"#@Z”}qFrIT}~I'?$l"8^dO$ I܁Qn?346Es.g\GBN9?0 C !O' u"a0 i쁓4/`wʃICݥQDn@L"V B'hhDh |22I(>UĠdt/"Z0EIPV W][pkcw9` |J&R|Kv)ۛ;UuTn-U0 d׿_6`Hs;3p`b\13l؅ZFՈ4 endstream endobj 441 0 obj << /Length 2596 /Filter /FlateDecode >> stream x\[s6~z2nNl'MNK> -R1w%R\Iɖ%+RE$AA]@ξ8{B &:!8""Ex9ˊz8R-M'!a?.^=8A/4`UITٛ?hBˀGSkH5OgQ p-I͂X+|I"`*$BV0A'tzC5UKwl{%H[PW @\uC_y`Yf'[;Di} dZ9i;"[GubAQb=[ඹ㤰7.3{˼R2I^ *P`8ᄁE GBA5}s,kCi;[1Nj,|7 c-1+oVWb+rY;y`~胱f TRgjw81&Ciױ$VΘEȌ1;f%#RnHӴ)±DjPB,J={jtVL!^{w"ʚ.?Ϋh<Wׁ}Zyu#IJg'F3m@RrpĞl9ϒ*WC$<$ZK<`©m7cPX`&QwPef6S6g-se- UcX[ llɸ#L騫(ځjA_ZTѢE}"vHW3``IZڣ=_כ߰fU6_VĻ]k(݋pizǫ)+C$#lbL)cmLNӥ+K홙lN=@F\Vx}פxWXjJ7StjZ^Tt"&Faa=LYW޿66t3? Xs6N5?KfE>*O7ٔ|&QQ[bP${|/$Dgmh xےo4aSB 8nCc ?U㈏EAM!\neDثVr`\z N0lX8̺Y-wGSb;%m.K,Y'.j]N%^p+ᧃ'7 qgrYCwWA6~XAݐǙӛM0NT:s#Y`1dfAR2$4.tgɜm7F$4b9p?rjȜ ȢXTԦd8-q!Rt=;>1=j3. In$YL4OS{(,qnHnw];!qGʛp9;uN\F͋P- }5je C6lalV~2~luς3}T_ Tqx=ݗLJGOHt8%]Mk;- }QAIAk*by ugIv#0gw,]pE&q| aN l$=K-T)@и0  X8Ws]^C~B3~>dBVI([EM"l1+:̎qkU%">ž+߹ˍ$;ݧFy a5-kAD%_t;; :zIRf-1ꆍv?[6 =g|?5o?Jw2"F"gW+LCHxLoR'p^ajȓi>?A5( endstream endobj 446 0 obj << /Length 2236 /Filter /FlateDecode >> stream x\Y6~_ 0A\s>9)ZfDYOo@')r"At7?{^_}{wu#^Iw7` Gg{yo;$&ݞ(_q."}ABݛ>\ {Sʑן\{ha+kqAUoW\aC*HƞB$%S sOIQ߱xhd >ͣ.x0-CA7My1,"Tso_Nι>yP0O9>xpoJc}sٟxC"%gGr^qbJ+q+g,5gC7q"|zx]Pk+@ÆBrq sy0@x7і\rv{2@1Uis>=pSOniE *~ѭʠ@ XBk?JU@R]|Nؾm6u("Ï4Z-T)P+SHٿw?:^dzhصӽL|/rL\(_ 3uV5QmWrr21զ~֫۔¥ f-.DkvQ vzM'[d2|ᯐ;Neӫ.p7"R:]iyBn^|Xb9Ny=N(3x}5`68na; Ʋ\DDa-S &Df~\a-]M@w(.[Vg[Vp5[_t]iܐJ&`ܞ5m-ĥ~<^N ,Ƿ.' -׆W]^D>,le9)d褘t̟gZWaclE<-!b k% ]r>P!z&D9~z9uCu;S)2' 3$naԅܛJ`ssb4;`s!L~C(;K"M!FeW[36 /D nR/~l_lt'rP{\j>0~L\}v]־,ܲ# O26) * x`wBol݆< SblmÞ}iT"rl_/L:pܭC0vfֻVhPVU9K@\ƒc{2 dE5wqFmk|Xя'Gvxm:pܐ" //'Id{|2lΞG'>h2SGumg"_+?_͛GRٸwق~ G}{剔oefdҋ?vtm& [E܀U) n_X0,{9}Wtz]y7y/T<1z7^vѸ#@UO2 X i7zAr'AXbd܍rHR:q`ka6eָ" MTN \_H>fE=/0xUp*?1%AV?f>ϖBczF]&:]m3Gs#*k}}nyNhjC u{}]I#^C<<΁=0j 6 endstream endobj 451 0 obj << /Length 2760 /Filter /FlateDecode >> stream x[YoF~ׯ`0xZ}Ʌ ٍaq'J.-`V,y+s= @Ր SN/@עwnu ֝;#.Dv6^9x d[f+a"]y ~$TPv5!եuAESjG!)'.[%b"]4N: ͵lEנ[9Q|xQ=J ne:׺b3(!zcg-tyюؘsDPAV(_W5Hμi<"%jXu:"=|X=r ۽K=͡Щ%.Iѥ>!l8mK_Ϻz~l;Xj1oK5iMa y::lqTӶN<j|zXsO(' A8W}"2dR߫2U(_CqnH W}yEpg k#?ƃLf7Khx~ŷMN-bUY=>h(w[1~9~7Khx^īz_!+ɫ}]5E?JwV?Rb;ٖZ~hrarZ9և'cdz4l뽗;Sfc'c8P[[,d.?3b]=QD~BYa/Y+ѐK5JD,cb0t'9$pN9nf-Bmdb6Mh"]ll!mWI3/޹Eiv?X_.:x!9f$Ox-nnW{$|)e.¨9?*}**VkP@?C;I֥π"^g_R[mwԥ9ڒNmȦ6S ԆԆԆO/Թ*u,L1:/VTf=t!+PO`&?)f43{ծgт#ls ^A|wfCvٍghyi>ٛϖz Vu6A"RG|W# endstream endobj 455 0 obj << /Length 2145 /Filter /FlateDecode >> stream x\Ko6WIP3|ZRtEJ֖S[MPw(R(+%r\Lcf͋ ޝ|wyrABIepy0,PFLr|TA6/zQ:&_8=H!r/O>!*@Qx*G"T`r } u9jpAU'`G*HƁBĐ)y$F(KX4IGI,{7s*zFlz[mkAOZj$ 83pmh$αr: C"%ֹ8ЈS. -uKgnq(u77/;FyEm*@PcC!Iʹж;<'D lK~X+aMṀ=G83lP%'‚Җ?۽KO[GƏ`1Sa^?IY63 ia"_`Q@&sbE!ڑ_H@ S3`sZǒzV%9HV`ѓHAü !&7o~$xh?X$ yg t#p1r`)uGJD-^qiynŇOD}Dp`ُӃ|3}~A*2E.V񱥻q6IZX>_ogL,+_ښ\vitk7wG"%G^Y<%;kj>rDte"c""mL1m,Ul:Nwh0Kq;_5`rgvDˍR}e|Q0dV#Ƿ}Z5h/()sLᄌoL&9t_\[vFY> stream x[Ys8~`*qdM*ddv)-Ѷf%R$m4@]z{t%^''b}QOHig8Z DaEޟoN>h{ʑ7}{#0b\S y-$ $&Z[N)h=c$*8Njhj ܽ'S4G=*_"uRK"(f5/E xwӫ+mwnOW} gfj$S9(|$} 8Nq71hEa5BPWMۂ&`B^!gD>X0¥ Kr:c%i}?@_6烞@`Aa=}1OHR?zMRi#L206oMy^DJhMiv=j3%GDM;#\dFO8!{[+ysɠսeNum5D|sw0LѭXEmc0߄k h8Eʺp>y]dufCN_Ǻ*4Ga=\8&IcƗV,"ˡLlJ~tJ $3 /eR|eHǬ$o>\7Vlg-WMm5&C٦`" @F~xTBB#1R O夈z](03|5,&$>+w-T˫|kX㞻0y+ R/)p =Z Om4J._,OZ[l S5H vH@]XDF{WKK' U1)vt7EW[ymBwPFIaQU;]D9<]0k3]&Mo(X]hb^8db90$w3ƁoGDF_8o;MR]AfS0{mʿ0v8Upם%' k=(}Dv7崙$k5#"\`h:]STcÕ{b`+D\8Fij QXj.R 8[['fe$;Gp *W`TNDZ%`Qn IZ(nXȵ&\h, f ]8bF;=6Ds9K CdQsJ?v"]7n8M\y/bW8N7}34X=tܸ r'`aKWE@10/bd ZR$.zSFv'y[>PP8Z7Da:hm:3g{e;<5eh'x{ݤQ uWl,mbKR {*x{)•Jq٭mv]$y'դjl pp1(|܅e'  =ӓcG 散N&NTf0\CES~bC|xp8E?߽8:4:~;9{48ʲSk8di÷oO_Uy"9 ^~{'6ZT{7رS۳/G''+&7شZ@$" 6jcLpգٖ_]chO T&{@Fѿ~_yB@5g" [O \afmTݵ9 oLB)+|n%з餥%^&3 G)lW )CRԎ B !/E0W*ŕfFEͻ,3zԄ NDœJ*Y06JfhJ":RG.z_$~O޷(&q|l˾yu=ZGUB_&z7ܺs@j0q*+P.,aX"]%/0ZOU|Z߮YOscyCs oEV?u; endstream endobj 346 0 obj << /Type /ObjStm /N 100 /First 874 /Length 1826 /Filter /FlateDecode >> stream xZKo7 бA#(F<@ Im Rg DZd3HeK ~A xSk!KPjx*Bij3@^@h `PN)h I xXp +X50R1H@Pjq 'Lj6b+|]E11+mnc@džmCjlե…Wbi|34L LK2 D>` nA]00D h[ώJcUƀ]RA F[ Nٙk WZ! ABq(Jvd PkP‘ROXW\!i[A R1@!и9X V b V] 5W5[39׸* THa&f%Z0P4oV6+ Z"04"e  `vv6qoϺ-$0Wvj~ۛXo->\3Fcm3t)vOz/zƓޠz9{dnֽ]?fݳͻMR_P]݅ z[B-AlzB:t.,B"|u/D:)0-'ƒdƋp|~sD)\r#Wt n˻b~+co`7߸ey$sŏoio`~7s|q7wYǬm,fuJ²cK-<&L m710ۯ7o=syvCv/l:v!fFa؁`βH"Yњ|6Yб/yF)k!D-[n_o?obYGi -͘].C"E3(+l1|UTTeYU%"2<2&ѫ@Ft{cBjM#, p4*H#v MZlBEjZD}(,:,j2dRq}}kjٔ`3)yu\"AZȤ1UTq[MGeMIh1\BRm$kJ~E)F&Fg7żZ bA:H줃;SM$MG9ioӅcԆxj6Ґڞ*kugoe0GO=Qb6ftJL:J>DC5-Vg;I{JTb[E!}hLСŸ6",}Xw54@r&~V8y[F)=>xJoΏ0I+QŭT>Q'Hk1Q~4ْWIt%*ɓvKn-EPrzު^WW65ם$.uϏV~*e؞՚e6ZS"Mǚ p?1ԋ]1p8G"!{L7gOe=Q{1}Pa>i[Jڶv+Xd&z<6{`V&y“&|Pm,o, S qB9+\-pُqBceUnDȪmnV@S5GA)i?Bwh+{Þw"_ԠNJdxt.A$'~t?`U}vf'Go|Xnl endstream endobj 464 0 obj << /Length 2546 /Filter /FlateDecode >> stream x]o8BC mm^Nwva$ږkEQ,2NlJ~d{b[D3`__1)$յCT0Wb:އzvX~p2jO >f܍Gz 0Gϫ{gW{t=ZH(k>aT}O=.=r= #c)KS?b &az_QxHdi$wBx-<*эП\IA :V +LJ^Hg@h4MrǎDD .o265$Wd6I9'1VE @T>^$,D- 2O ]q4Kxyv|#X` D'"q^j;$: }eY`Cb *@Ktf7]G uz~~ӇC5k#C.G#gEӫߞ<* Uٔ\Y DeC*/`Pz1I8"dvĤaNߜ7N_]>c%II\,%%;\au`zWdHg/'9' `kA 8ϓOiW<\fm#NjVw`^ #\l3z1 r8nUuo|F5:QI~zOQk|7Z}i Ӌ~v7NvC"βGI / >yEBu%v ܷCl[`Wۄ'C8٪#`ih`N0GJjj{n&+3³;-ߜt_ORj(uk U44]l?+A/b&fxsab0[烲@OP4+OHwk0/5\d5r[ٹϓ(tqVg@Z 'Q{2q5^T.? t_@3"Xd>'&J 9~9X` endstream endobj 469 0 obj << /Length 3144 /Filter /FlateDecode >> stream x][oF~~z2`۸du\tI!P"HUAД-ɒ,|\Μ F`ǣ.@@\^9qGԧe)ދl4i?彰H[]~0 Ar_::<ȅQ^&GNh;7aEp=vr آ; ;OqK1LG+]f]~ttK#3$ @YK ; iNm坪mŎâ%CitxqǼm("5_%MKF;WNy6vj%wmyzӚ[8/vZݴ}BgQ)ǁ)o7b 䉵bᯥ&ٵ9iIZyᝒv7O>EDp'7󋋷^>ߝkXeg[n^ Cw?&w?09{zpw|#Zt?DyhĶC} ;ZzRr-g0$϶~`h8?{z%0/a`+K{GXӚ~99c)uLYX܌^^eJUveA_鋋DTBYoj}$KjJ}6EV|-U6+d,"tgj5M+i˭ctKuo }1"53fmdMbiQ#=AfM6s%asۋ<#淀`ɗ?XLBf}- o>)V H'aO' vnYDFX A3woP/q|wS%nC-﫦0pWDwei@i\`; ]%d!{Ɉqm ;>cpIuq~]ɣl2 ӖDESsLGmaE+c])4ԧwp+U7N!`>$}Wun;M7<t*{Rt,Q,ψFSJI۲A~ԗ]ZoD&]SI2 U'H^iOt;f'gX*3H9nI?Jģb,ZRGi&;zSj[3m$L(^OfHJNNZzvaxRΉ6 Pa/pF-auNe:SJ-yaI11\Vc,Nwbqi!VJShKojݧ]s~pU.R}6%1~bF. r&17_j2Wץќ4 Wi0 - e:3]5nyg4%KF'dyELrLZGiHeRh:_M4ϡPF"&i(sYYUv\'@̦*)TE1h”]0pQ.0I6(xHhMzg9 R6bWދ,iV?SeEuZ99CU҅~a2/%_js]t]R53Ca8k%&;rPH #sq7M<*C+@IÅbB3j^'_LNl[HĐnBN%> stream x\[s6~z D0$;i:٦N4-1[RHNf}@Ȓ'MLB\F"}ws!*JD ѯvt&YS'E̳:Nb¦xSшE1Q%QiW'FQ"$1VTX2'ԑJ JYnkz;D4%P+?zAvHX#eBɥƒҵ0" ͊)3ʘH7$K\nN_51}akh7CA@o5^E?G=BEGyӿCUZxƉp-^`moVNԮV:Œ4Pp@*c6߆g+(S@yZta=tI5IiJpy J!'Q:S\M~^K us}{\=%#Y)ʭُzw B GE-qxI{=g)MG3.E=4˥}$۩%`zUت|*TI>"TQGE^&])B=[:|׸(|ެXNrSWbF'j́RF34, hzeIج2>̀O [2*8J>8j/D)[[_繛Yc eբ1/7ظ5@.@AevN-fW :m/`1%L _:`[ |KCSc9+_!e^\\튚zCP#ά<3A7t@xCiK*[:2G-?ZF ^5Ǎ<׆|> Py-KYcO$$@r*DǍ *X5Ƕ,^\fwXI`#S\ʾVT7oH_"<`lWNē(MClrV) k_0PXsQ,FSrl\@X8f|8tS1.X*QGRʡ6&h$x +_(@v8*h 2?*M&;0Ţ6 k;<6lreE $eO^9h/GLy+h6yvH:ȵrS f7rmۥbR渧nlmFĨ ~+'6`YOlӲq"䰆Pj+ m膡# `ؗKgU5o\[ƚ+n7}h; ,hk+[cӶlUclߖCy Kq!m`]f*m*1J]KS St |dm懔 _xa1{/u12[VXm!uR6%I]#y7>[6G /5Dĸ%@x75!b ^GLUf$zR&+"*D^ mEqn<|q]zV`^z: V OzK]kD=pv}ΎOwPz74_VK{s{D}}騼 *6_`hOqh?=ehVB\W"؏-0mi6hbci3k3)l7d0eqelӦ.)'['YrP9n\@4& Iƍ6-҃2}yI\F4& ( &k; V +M+++5w (iD<&A~(¥r)4S_e[d<}S6"ÿmj |G]Ej¤I. jwVyh'|ڃ~u nLΧ) m:7*nǰ0DȶvOoOIHA߭uw"),mޑ$S`έ*Wm4(` T~pe!- vevlϯw)f3nGuw}1吕a~mQu[9œo̠Y&O`%Kr$o/ev^T72fKú HGHt0j3dq Yw|îΞ#gݝ9AioGs?yN?AW,gN #Iĸ%@@(xl!#08Ds;N:8BFOFx1~yUSpO\ 0_r|~lטwdЁ- Boٯ=] XsPwr*mX98j]z5/vNcA8i_}5~ᩡ?=x ڞX_>' ,#kg"`2N?(fOvs$EyT&'V \…?eOy,qM ϼ_9ijtǦ}4þ^\~ lğx<dp؝9&T9 ȇ-^?؅Tb]|\?2ԍŸ8$wcQضķl/{ց-`Q &Jz=saڈ{z}UnmgmFN|^pI𱫢9KwX yL~R#@OkvȟalaApC2ok'&⡘L'icsTB/qy`b(!, o/|IgE͉pvxl{n@q?۱~ߋ~ȿ &c8"ә!WG>NxP EJ endstream endobj 481 0 obj << /Length 2590 /Filter /FlateDecode >> stream xiocDIN&mЇ@40hpEIPكRJ\ r{αkz{{ƽJb.Gއ޻_?~|Ƴ?(K}<46|D/ߝ|y YG̨ӓ7wF, O Kxx;[(-|(h99| 7,p>˒2Xny[}*z|ښV@^L<ŭЧo92@0OIFޟ 96jcM.)Eh8 +%)|av'kgH Eux[p w&C$!0 l0)-߆Or.ˑaa0>~Lv@Y%݅aZHŏ۩L 4ąi@7ү&zʻhM,^8J#ˋ> _ @g:h)%Lk+mFY9s@ Œp_CQ ʺx2Q[Uy@@G 6ZB7Sioq.sGt\y2X `gj&| K<2N&DᓃrzmԋFj@`%)3L`/Cĩ)|'>)* Jf0ȼesVLpij`%)-:cdU2P0|Ԁ(JÂV7}{ {(Cprl'4`7fqgwg@@)*].&PK,].0HlP1)};/_ﳹy++*$WH$emjF$(Kr`_AUXzveL-zQ#h!U3q0ܘn퐱vUC2wUfIj-Wiul'V0,E.'GwTlc+h@:PǺ# 6_*9"+: `3sL82z>.>O' egm-|CBeɖmYN[),Mi*\WvTq1Oubpdu[w) z[$noUHo-6 Ởpj Ta蒫U^[gS/E+~.P<;d fnIٗQRFϻA5] ֓?8$P h6_ddWي _(?goLj3ykᾡ6QN?q^6^3Z=7_KIK$`ঘ4!R]wB'b0Т,@w] .#:їl-C2R!!8]<@1 ev#^:,kV =j_VIT>lPƟDfMBLDsB2:D,(:a^\z눏k*"mbzj}M~4?T *hK,8GnYIm ּBKoT #1NUGqG+Gv wa|Q$;GVLqd@\|(DMZf3/D > stream x\sB3 orI$LiRgK憖hD:z.vmK>zM$. DN9&t69-rar39_L^Nዯf?ݪjgtZn꿯UoO:?DTr&VèH ;N^$' v")k5IR%}9w'GHR(jKT_W8)RQ/p8קn7{%ȳ & \UM ^FNQæRQYuE\կL:Ǐ} Zg*^SêRԛuyS/^%rڮqY =SJiJˤit)W-U@Տ2:nTFiWtS@Mpzၑ row몛g{]oQ?43J#2V9OmNBgDvY/\l4ɼ ,:ٶ#53F>OWe݈嘌)'ӄQ^47(L{5l"<;;ܒlZ.T/N0%50gvՆA\\}II{i?o ,,r⒪5f-34kCY6f͒0՚v*Bx \BPH׳) $JِgB2p[+,lZd2xc ^}x޴M}I$!ULX#(0˿pc?_t#REI-à@ P)L"z zΓmJ2BpqFw~h)K*XOҧunWNG֑ 7 UV-&n3PNBq> 4,i~B^Lm'YU"MlNʤŴ\smn@pf (XqESDF MBVg#8+> /fQPn5=eQ!9ɾv~@z:lLt&T0@tbp]n]{-g 8ugB3w vfX趮V{vn7y0uX?uXz N%oS"I#̓PV喍P0lLd,KdcBg35EKe(ݽy+\fհ8sG/.sW3L6a |ti1 `J%UʼljAl?{Rfe}uݙGؼ>;!"Uٱ3"l#I0P }x1F?@9Ngnvzt7>9r* ,U 0k-/熠A<#p麬lN%ǔYH}9f+ /7k=g_b[^m-V(:DDԑދT3F={F8_j~v(ᚙ!peo!2MU ͜*bkmG˺%1ISд (6/oP(+6nI QlMc].A K!),`N9e06bNuYP2\9z3Mdc7(QZL&r"3p&sgyp( }t2 ۝e@MF4@Yݐ O^vA+z!dPXz({C?yqQd]ِ5`(rYin8裇<ge7$dž f)Hs &stDRڀ7}KL4x%"N.g6cw.&?x΄',a6#g{)/.̹bokC?_s<$#vȚL݋~";͋e_f b5.쒞Heft[.w "/Qߢ;! dϤѤ ٨]-Rf橱ssr2ie7`ƽ4Ql;AQOiyPK^zA5VNbFRZ4E6!. AfhAc]]W "Q`FQp^ $fG큵Tx \)(aH=+2 /JSPӟ9yF/yz^m.iޭ}hmB{-^?Q0dpXM,wD"bZ]A<$-ASZ{Lmw; W2UxF Gђ.`;gƸMfUCDT%7#(ڄ{+ I G)s}5oԟfK$9 -PBIjb46yO!N܉f>kQUp,ߘ2,΄aTލĕH<8!M`,e Z#])P+IlY#WQ0y;:~ȷ]O8$;QH.b; zT00n} VɣE=4p7՚m.36wO J˰ū(=x|zW8WF M?˺tQz$,)HhtfḎ(gR8%Tj#T85G 6 Y0 X 3fFpWp+a9eVs#J !%Lc hXaB@>)El.qQ24?d<>Kcm3#!w|h`EԩlBn*eVyZw±`ޥyd<̍t,דiČs׷8OVS4Ҿ_OH`b /,9JU2Tv~053,rJ̣w_B6*ZQXAVFA *y ájgJHNGVͼ]@gga#sC2ө IKθ贷 s" X>`utu&b&]l}Ch+I> ~r gso#^tgRʃxF /(=(G<(#Gff[?-~@T{:Χ˝܉ϝ8wbr'˝.wb?xv]p~vel9s' !dǑBԀg֦WMUl6|Gou@)UpQ* IRt ~B=1x;jɯ!f<5#n ;;5Nؖٵ>IxrGA|xՋ}ի~瑔h1 Z92^Y[7%pZ vuPIgnS_ nNwvq#ULd離 ~3.udFD=T.U]opPi:G%@H>}18tO㯕(!IaZ(ŷ  ݞ T.J9!RXZ endstream endobj 491 0 obj << /Length 2534 /Filter /FlateDecode >> stream x\ms6_9i.ʗ$kI'_nS>CіT-H%%7ufb  ,v],޵7gߎ.^3E(zA"2o< ?dHp$q1˖uty  =~|^Gt(_7F, OꪅA4ُg.R (Ҡ&% ˨PC"Ev4^%j:[^eKfVϟ1ˉ,iU;7ߋܱLI\}21HM &U?VՆfefra x5h# =[w*߽ 1Sۈa<ΝxGx|]i)c'x 4M =]!"J9Jm/fZiQ)r PBv(-;BvUE׺3Oʑ! ?J:y(_4.30$\R2z!V ů 9/׋odt UsJI6QxͫO%4="0!tI@rYr1non or#q gyG5ԭ}tTΆA9:yct]š7:5:3 RUJfⲵQXΆihÇ9Bg@u4.1lf4M~l\?\z/1I׿6!t+E%EC@  7`@Dx 0ގ!(^pRGDK#BBuZ@&Xtj$32ߌQJ.B'P*@Y>=D#Xw(dhpb{7w8ju): .կ4wRAe7M\L |Z/ $!t|Pb겅 >tJI2kwγY0[=:(Z[a; L!W¯yn@he˫zUFc3M`'MV<-aHਿU_{.www 2 p'Gr(#]gi:ge~`ECf;i]4(%׍P _$VӳB<~, ;|!~t33-5tlpKdGVv_4W)WI{ LjIO"΋gSlxǷj=̊`)AQD:Or7Mj_urUhtU:yH'Ky''&"5R JOIvx0`@z 4fu:Mx`:-/̪ni}]&-/G#I-xq1Ɏ q礿x.u7zXvA45>";Vrb]rzhBWyұ-S;>XHm^m?ձZmJvt %;VL6 -AtBQ45K飌EO%YM+j6 Qb0)邦 'H4@ʂ3%=4@fcJ0w/ۏaH$hc!+q;aR3Z9~G<|Ռ1astv7cR|xl| T$bI>ߡ&YN;ŋ ,)Әmjp ]=4yD %h{rU3)=rWiWS ъR&(kmͻz`p$y⃎diuG7u䐿*͕a(l_v/fdۓ/UcŎ|QxäyHLAsp1a'L%5kbH==w xx-C$gguO֦щyM*+e17Ͻ|GYƜ5PKW)d8&wwkuDh㴲ͶMwvrDRy8s&l|ΓR5;=rj-5't><@xZ>>yY!|ўoԘ| Cr"*Ne1[;웶]E}ҵ|R 7N5.&8~Sھ G Ki7C1(zSW| .'x1ź/]V3Y%nPP endstream endobj 496 0 obj << /Length 3800 /Filter /FlateDecode >> stream xے۶}Bu2SB@$HLumw;I'M<\ZHL>E}>! X>ٗg\}TeHf%,e2W8ʪ;_c(eޖ f_}~qYKY4fU,؝#-Ռ3k3j7SqĄN~;ٷgܢG(fL'ő&c^UЊ|;7幈lCk!BJ0˲H*Mԫ_"ݎ$eI$gIDFH͐ӰcIˌDaSJIř}1<% S-o 7 eWog,FK?ͼBg!%'>!I,wE!zH2#sK$bY zdqTwEEul揑)qB _MG $罯ec {d Eb5S2 #Ov~x4-@"r)zR4N-zC¢AjǠ7 R&u5|Wޓ{!8aj/v[xGDTȊ(f"ߘw/۲{63 Qi!\(]xNAmDT݋jY()#ycAڪ#1Gc&ֿlXulxg,h^y~E| բjyNj(A!h ź p%Jap u8W0~L!o.]IY"dn"6XūTX+V r!>:)ϯ`׬麄$Lʢ{oKW\5$= HWR@` GdDxE&{J*Eo2 ȓ[4ӽ/Bp .U5xWAQ@0 )]ey $SWղ|<(QPi.߂MG[~%jv/ T<@|Eg^9*>cD] ܁3xw뼳;{FfI@j!)q}e|7EnO"ozHK٦>O@P\i(Yd $RsB`u כn $/4bĆٳ6*cRyw6kD7Rm+OQZ1d>y{eIFLI"yPd|E˶:5ʶreX{!̤S=!D$a5h&J^Jti ;ޘ$2<2oe)7a&[.>/K7Ey؂sb 鄇~ e\]^)EYO̯נcIap$}i9Q(<ֱnV1ny:a(OS&9b>z{3>10Y $"SЁ43:sB+ YeUޒJ`_Cti< #M!i ]^jlUG6GFeP墄M3uҼqҦu#'vfe9xKQ.8| \ (fIN~Pcp}Wz9nZƄĠ 0Ъ*UԱpIU/-dY.ˊ J y7Lz /h4CB`O& ,Cl¢xh#JŐl .JS4[Yz ͈ܭW)M]Irʾ NTWOxc𦔇W.2ǪK*Gտ*vټ8@0 ?}:*H{`-$!UDk;b8f +v0G8 VSA X]IO}Y8 }'fz.Vı+Llu {|<À[B3p+zӺdRq&u4QBV:l`9ŧGxgoG.G&+]'}!=W>M65 &r%ά7k{$ۺ"'%p=Bo:nm)@M ^&F.MlڃkQ+rȗ3;Aa>#n' 1r?JƹzRU \ RwqHhK$.f_ۘG|I }Vf GɈqۆmOR؞sL_N ζ%:蛻% M$4p$C,,ף=_v4ݟ)ZWgg@`j IҸ~M³faV9zŻ:KNʥ\[.O&Jី2"o?BFܕorʴB^$F'jtGcϞ=I0lΓ_i0ڽ+[N e~ # >~dZ .PCZtq'-s ^@E d&Z #ٜ5;kJj[kʐ%27ͅdzܓ.$UlBlߟǺD{]@iuӘG)De3ͫ4hu endstream endobj 500 0 obj << /Length 2751 /Filter /FlateDecode >> stream xksܶ~G›x򑱕ۍ'IWЉ3GHHv;QIm:r} SgP'tBzsO;~xybMWr*- m%QN|9xy|,afTIT;_'kH=xΜ|@-6+q`31h> i"t狥)Sn-r<eMcr<Ӆna9^%=2ϟK̼E^Ein:IrӶlY% A>#शs$mj`Q/P즩ӏ FڎJI\YĿ>Ua?ɲbD5ͳ0\Gՙ~]FFu%$pL`<0Xsn&AAqLNE/I /@4 +ZIS~FDsmqoxLN wSgIiWCv;bCY'۸؜U- ,jҠY_zcy/|Y!Zx(h rHWjT!Yad#Z JӘn=^ԭ Xdfz=c++6+dѥ1\I(XەýԊ,>ۑM y^ۥ0K`x!ccl,F9w~FkZcLeūhT31m1Jq%~k碦7. DŽ y杏 Q$5F{~h/B.=|M #B '7: 0eLڞݴ,qޠ7m o>'j?TVJLgZcdl }7Lk춯x 8TwS΀/ K͐bGoʥ/D76eg)%+;ԦYm)qRa@yF )3>I_gHpƞJX}X~|lP7p,\+lȻS(" LU#ڞ;񢀬fzpF_="|w;0#^ `}Qܥ@=]X<}c]Mx$fQ/1"}HY%i!'CIf>^ƁBIJ74ٿ3[s3[RDCvcEN.3ӨVIY Lvv/ i]'^F|'\aݾw xKL|ϳЩ,?ƇgÕ>?|ɰ``I@f^`@m'XZ&Y3Շ\eaRtaR1~*m%6FCt!c" উХqr{/"^{eLҝYE h@v1!M\`ƯRAG[᪸F$^'^$w DtDdfR%f{tm]⏺#N1͓lOF'V 9ɺtI9T2v L~2v %K&؝K‡x.5H8]bt+}~ mF$|ֲ͖ذL@ۦ+GGGTg:4Khgdy$֝Xry-XA*P@>JLy龭}-/GIϥn-QV eSdՕ';~>}.5(Vk+Vr$-VUK]@c.z .4xwҍ>sq~3ꢋy0:v Ea6thhd}sl |bYr'W. ӻZv+J6d=0l4>M=|0zi'Fy̏4Vwq:!cc1$G޴#$΢mb($GvGVR }7jH6M'IU =M`QlnoGjB=jwIidߪ>QS׿-SM^5MsR`-خ` E]K}ya͍ת4"]d"5Rt5ɇ3r%H6[r.,opo0_HN,ٿ=xur\+C9 F U]!Smec K<%~ endstream endobj 504 0 obj << /Length 2343 /Filter /FlateDecode >> stream x[s6_sA |4鴽4qҎh[DU-$(ْ(YvI$ Mξ8e10b:ʷfQ%~}8 ;R1"TpGsI\qL 3g/j7Vs4#I"%UDq#N4-.{B6:X4j>Hi+ J7${w`LߘV3\F`6 .y8~`ha6Ի<-!@eJB IU){iVP8)ۖDRd0$LJ{+< ##]EER#LjTK(fhQ4PŨ%y_IJӠ 8TX]]]^OiRy6:`gUnA!] (G++|˂dA$_@3M4MP-àc$?38 2Cd Iv07+}\ddUj>"_jdiiޚ 3>MBQDaބc׀ u$}V 7' Dm'U۰idH{wA<)ve $t Z #))5Cc#j͜ aVVM@\ͷMK`+],^md>d7x;Ͽu61"s0CB hE'5ICpt6{,a:k#C;7loЁ;g.*4a| nI~ &@LLp(Ktk)Knn`!}-3GB١ِf CaIRYq30: \+"+qm0QVq/ J^/_调QCYZLf)x팒}鳯lo$&GmZEn|6[eQR^i޼Mx9Ki5' $4OF} >%7fj^{Yl`݋ ;#Ԇc< }c$J pez ͨUkB:)Øy;0a_tjYEAUT1D 4cU6hf!s4mq0X̪e$$yIrб3x^K`S踣-6F|(xW!sA7xqHP QjGUo-LP|PwƠ1 fI9oEu=i8r)ҌBH ?֏ǐȘ/1_ Yf5gS}hZw zޯyf[1oLzukC‡N?).yRvbq{$|DH`cxGA'W+뚜N|:UvU(Dec$/?ю Dޥc8Y"Zzr[y\ n A= +ap.!ЍhқSO|&/A;l5J'1#1T?CYH]L6~lh P-˭Mlڂ "IrvBNbU,kVuk'I$24 QXPGɥ$pdcc H;:iJBo$d1b#7&#Pt20h-5 yKs)%]=t$ƩOL24&U9e-"Mݭ-HVne j?e)]r9B~h0Nae Qq`VqLi'o yЅTAUxRj!X҃͡THk>JGӬ0 A0náth;9' V/޲ Z;DCl;ۡTuV{l.֚ *XsXAs!i*U+Va[cQ,VB[slGO_+{ 5nV7]#VA+n,5K;&,\ Ҫd,JfU> stream x[Ys~ׯ@i@V\Rv֫Tʻ@PDL\濧{BILc{{zzԻNjb<.^Ƚ0w1>M>錇oe]gE3ⅦVYRgIHgo/~?c0 U?t}7́Dđwj=3Wg?Q:h8 J€ԧiY4y˧EUۦʦܟ6)4a 㾌ᳺ=m"0sw(lIuA1]{# aF#" $|aDH{< ޛe<2 ~@ܱVZC|}Ѝ06l$2T\D2aʚ0p(SewĄ2| Nl^8uf|J.-j}uw(1)|=E!!7I&D_F}`1 |̎_}h9h秣5D*^]KrTlW/NDVͷgKc1)-} ($ L]1x>X5i8a/ yJ0q.Ha cMFw0jѧ§ XVcU*6Ό4r1v;&ܫ^ h?{ jwbɥP@!_2 # D,n( ,/mW#ܵd1uΚQFCQ/yWGp[?Puf٩3rUvyʹ/ >էoGXl`rN 6j[fYeyYwyhXLQ|>٣hvR= i b0c3}be41f 6A5/ Ĥ2b (zەNSN'*3u^\O23O1cv3&$ԷՒW:ɯMFE#+^: pu>4fךfZjIyk\tB U*Wy 5*/>"."\/f@xS3Q~Q9.ŘUQ+S*?0=4ϫ,mJ1vKMHzb*\O'D & &IQ_>,&8_2$iյ&ސ|5mVqޘJ .>9bt7G t+QlBFa bMbo~_ zuQFʑ1Ʈ[M4)Z("Yi 2RO0ڐꂑa}*U9w2r@>R8;&26>B"i`Xk08D il4BJe;VTDkVΠbd@c|}k: c V5/ߌyA\eĸ{YHGp(K ВV<|0Za<|1ZnT!yS9_BFv\&д+2uVZżx};n-բ=,ĪXWh ]MڦͶl 8' NfOÖT50 XȨj],j}`,z/Mu#ZU;K4EgF{uT=ەeka;b]E^/˃N dH z GG=!gK$핪1r 'J N[xH*#E\ְE7.p>a2>80}5o'(h_c=n[PY=W1>b 6&WzCÑGB!\1y܌u1ݕ5v<Ɍݍ=(dv0dGIN[C&K]t>*۹ˇ'7\yPz]Dd4fš],o"I#7"p$5l sA.A6EΪPEzvJmNuϩ~m?qP~| {}-}6 lInݙ`<$^U9C^}o9#,EZ>,:pw%5NbÜ*xx/⁑G9t:{}%ΞS>RfIMߝMCq}k_x,Sk!J4 #ݬhŔvƷ8i҅qAG=CW/ߌك1(b7@64]ÿ_wLjਟ1J̬KBN!C4]&)lLg'?`7I6 2qd H endstream endobj 514 0 obj << /Length 2585 /Filter /FlateDecode >> stream xkoF Tk{Hzn ^CRDۼDU>ZzZRvfvf0 ˓o) R&"i UhDag}"߲bL>yMoQ{CD''`X* p W2OC(7)= ޝx:Pa ! 3B|H"CA6"X<̲mMg὏iV$𚅼\a™ 6xWk!iC@U,ü8I!Ȑ1„c\'楤AA֢׽.Yrpo 򪤡)9ȆT a` woqp7xo#-B!v)Ğ!823V깕]$,詭 ?} 6!}\3 ;cϿd fɿ~>M6Io3YlW="TG!c=0x<͓S 1FʢǀAq Ꚉ [~{}cDh9"USCq;CN`0;/ّoloi>[$; ۤ0~P$F#~ '\Jœs^Jr ' #! 꼮VT"x qi ߲-?LkWk."08aU0UJ7NJUil}ś7W?xd4ʶsjSJFZɲqL;wwm~g)+ =m/ipm+LONOKyvl6>Z Gerlek10Ⱥ ]Zh"?ak)̶IJZi?yE`āӍ=L/^C⟞mYӌ~3 ™8ӟ;ʹb/[GvcYj3cu3 NC}!lXYn_(/|LgD'ѧxR`6JQ2ڼi)C3 )&% 6[x04Uγ>@7=P;}`K }Ɩi|=*@z_!M-#bgLWhuj k tA(ty3h(w 7Vsal}|.Ǎ#%rR4D_# H R'xV} 5 5+q$KCva8 %wS, @z*BRVtp?{h%+2h{Y{ g)q^RE &c:Zu'*nLjzTLG GOB$9啂[B!J:s]~vLb@)a)&t,'|p!L"Bis ˍF> stream x\[o۸~ϯкbwIŶ% evO"Pl%-IqpEBY-q6H ̐caoG?2ȗT:gWKs6v^xQ:~NX׿ga u"wG}:"0 v#h~t;c`|Ϲޚ;\D] 3ϣ?"DbD%S-%qxVpĸ9cE12B @ I&y)-qq cgH= 3tmx Q"H`4Lk7h@DAK$5(zـ [w/E8(_NL;-B-ٚ[22n?ƺn+-[ItKMl޿I!O8t25ME_@πY fPK2~_$OtlWǓSPr"9]Õ哽eM7_JLXCT2%a/WR/81A`Y7>N>„X \GԸgfML'jrgPXe2Lth [D26^E/#D-R p%:]ZvQ_O5IB!QO4R2]* {ngMT1|۾@"ԯl*=$ s(*+Ɂw}5,Zq4wZ@z^\xJyB;T Ւd_AY+tNe-K|ز W:ŕaG p\p2.[%ª~kՔ(]$3y)kɌM#)@BEGu9*Xrɤ`ZfV]֚#`(G `$TS; XLHH"31++i+|CnFU^t98<9p—P 5=pOWitf(-[Cs:"H;\:5Nn: mj.a{UnY'ުn{(݂Kd$k,wi޵j2lj y]~sаlzʊAAUI73_$Z)7墼_K}tR7P:,|,Ǫv zۚ33w{h]z3_g4>'FxO5>R0FvLB{ТRuwájzJtU Xwe>f;]({ ~g Ou}{CLȣ}vo t{3~^tqw";U˒->&V m =ل z?u;I j=]as:wek>M,nd6}8a~k}ILmWaE@1زMDg-5JHS/ݣEX=@۹lўȃC_Ge|U)`ޅa^SmvC>hKںh{AErLyWAíorn)>ȖI"lWe Ja*i8$0-0q.6-`Vp 6&+Le\@]URզ|m*e7r:g#߭V0H TSwg:G .`=aҮAlOB9mJyWl4[Ë,T[va^?R`C§[:)PzN\oSٴGP\4=QwPO8]O{/աA/ucsDP&T#5}D#z31D"itu3;' wO@>+NTٛMi:9{N/7$~z9AoR:N=[Er+^'I'c扫ēR*%SI>`1{ZlNm-dgz.֓]2U 3+Eƃ&*wgJ+Һ[ 7Z=R_d{LO&WjOi&>vU ɒAx aSbe|ԙ÷浞D$Q˨M~: N:Y;Ν7)2[)S麨RF'vNvPDEךbH35χnT]3Z[îH?֘J~ʻyMyZU8+XgU=61IɋmO^u= бeSHlVqe 5f/C=ѩZ8K@\-e?.-o7V39nڽ7fXQ>0IyMpF #]kj+*N立'&q{2};$݇ Zp~ƧIzH}rYH@={yl﷕A-A&Uv7SLFV/cQ %RX;E~W3ny]#ncGd{}ϡŊ\o$k?~hG eh]付]r\b%Y.,GqNU}|h)a2ǝZ}*qGT4[8E/192y,j_Ech0U e*Ui0e endstream endobj 522 0 obj << /Length 3067 /Filter /FlateDecode >> stream xYoFݿX &s [l4mb ͭD*"'(7hY#9]]3#\8x{71%eCT #"OwpDh| )<;L%/ ?wɧS!Q9qL'~8 nLyA4Pr!CL4ȜRHЂ <ɳ2Vj ` <\s[Z,Ր4_&5 YiA nƻZFHX8ֈhksl[u^'9ZH0 5\7.b mU`,)J̾ێ͔2%jB^?$bJ@ cF$"5P}B`[J*tG2FS֠z 0ZH+YE!U3{5IaTZ4*k F;7X ֫yakJ1K[]ӔV_ _a$`OBeӋEL*͚xbq$a!DTaiTZn" ֓H?]ݹ(Nl$C* ojeC:l_<E ⛊x` IN`Z.6a(eGk4v6䌞}:vi}q: ٚtmbHZy24JkR2 ڼWzkH`B[x4>sGo vKrF#qԙ3q)o$`j3DbSбfVyiע+۞pзBw~t3Dﻟji =R-vi]a9}[!6ϺG.-bUl$o{Q/c8}_g/jWzP6zZ1Nt2lN[ mKف %]GYM՗cИ0DD N6xGSH}Oqlxo=nxWם-&T]\3&ً!0Dmvow܋/Y>}Jy}!qaGum@ʎoçF5έj=}V/N>sqtj9pDecϯͳk_>bgʍqJ e*2L1gdrx-e( l%TʹMN6o:YW֠5hǎcLNS^9(TX7SG. !ȶ8$}%9o;DB@*L+L7qΫ29|z1*38ѽ:tjT6ɧiv7כM;o4v|89{H;DZrTT߿ꙝ!4ɃW{לӝye?h-Aks3)5);[dDC&]os6~PYypmԞ絠!,FLtR%󶆺fYe'E{%h'EhWGhGh9y^>wMTNke< #(8<@Cz@'d; R& x> ߐ􆔎*mzhԆ| B91GOl0شIFn|ޘVI'=x77GqEh?@w@?ȟN(rBEE&r#[Gv}5RxrG-'EA- y @%E!RW |-~NΊMt_5-ڭu g~M2[ |5kzN4׶ݤ0v }DakvbL$pۄR?'|f.f: gՠ6})gTO[}zxt z7^ ced00Wi!Wg96}&V39ꦫBɎx̶*U8%(j+/={dJDlބZuO18 kp fD8UK[nN0t ri2Ga`&6j9oc*i_vfu hv)PtLIxU3G/$C$&5-"7|W u}],h'p$:K rȁfN[hY,lsOZp؛Fg+}-\B-*ǎ`垩]'_beeT-m8W2 gKbDCq`N;ʃ jnAi:QI^hoL.>8A+$׬Lb5SGG(iwŖ5]e!Bkww&I,_i6N0.Qbe7ͳb BSZ,\D}}v *pr$ՔG?5i38o!bO՝"eF`ER~y02i>e!oҰ H)kx.E-Y/P73Z%C4EZhsP=!v/G! endstream endobj 526 0 obj << /Length 2208 /Filter /FlateDecode >> stream x\Yo~` dvr EARuq T%},\CZlUY:#8 p䇋7?3($4h?4 i~Ίa(I1˳\:Yiנ(BdŇ.N>! #` ~?{.q[6gq q'%rP Is Ry$tҧf$  rc>t SkH\z隳]W]VL >/xVf[G&ZPJ Icb* $A:\Vk`^ RlqkS }Ppa+M7Ʒ4eăѺp$We6>A,$$)!<a`{$&]C! azGLwl¶ln*ļ"EkΊk\꡶i wwYMBtΗ=+k+?WK O0E 4?f!K5 :0]Ɓ΢2,3+Z8*Q{fYvl}t*/ [:--eiXM%VU#鶀 AM~ ZQjj9G֜g>S=kSn 7>V%4nk.j˴KhRn0RYfd'vzh'+NScV$7V)9#H>zJ&Sq~݌<*Q.3~{yfr \".ʠ;b˾.J 3fv(wa+ pH7˸3Vb%t>AFj G ;$Se,ESD3wYAio4H7.#B޹.7%aYIE0%aw=YcשD DqlD. P.MC84ZnId3S -!iX&#Z|)a3qʯ>-JSKGM֨b'O၊RmQοbU&4mg|O&Ef.Iq:;ڦd*;HDQ{aoL &k{s{b4)Ĩ2﷖붯e_5W7L9jѷ;(0̧6e(0t]MgWߞR>`n 3o2bRaGP0,Q:Y.>8,4Rq1ˍk(QN|sq;qڋ X@* =Am _ni%fĈͮi?ؽY\&վ!]CgO1b+t?:!?`_M}l4hÏgS}|=Ǟ>0w8Bw#Ǐ=FC=l#%qa1?`'nd􂨃_%/~]譎.zĢTS#t`&{,IQ$ņOENc۰^G?(Qe;Q $]Wefl)}X'㣊z|]KZ1"d3vTLb*!EH)Z S֩(?G-ur]Z뛸 E\vE~E) *!VzI kPk0aY}{@VwlvB1;oyR@pDVQB+ 6NC1(}+2 C{dc^!<-b,%iWiR\ endstream endobj 531 0 obj << /Length 2496 /Filter /FlateDecode >> stream x[s_A>H3a 64}4qҎ l+* I{$0; g,=ػgO_0(TzW Q<̻zo/ħ*"ʉ/Q*'QE /Ϟ_uF`*OQʑ/)0ba7o-<.J{}vbD׽[H1ᪧPͱ}ӌsI K"tObэśB3^ qn嘺7Ӯbn͛i.AoL wF_$ Wv." 'Zl$ʨ-\*n8m&I5TSNK/F#nQȾWMTv*}"Lʤ@}Z.Q@,؀|BpO 5Xc#-"`cLxxB "qqÐɢEb)9`j6SRRTbfVφ_P1^F+LVrI u@ X vFz1\FlLO;K6$4- q$I8;$$~75M>DG E]5 '65Q0~UFD]$4\HW%o8չݚWVot:Q׀$ dAU@_AJOU-zlaiQBVT/Vꭈη:!>ԥkQ&\[`QM/\j/Xb$%"lD!k>eeY 7g3BkOC@<բ}^HVlTHB ]ZvI m`oUH$k8w]%,? |}Ka v(~"U|F۽і~II6!CK;2%L,f!wx''x7Ԡq|N<{-76c6s"c\W>>crד:`3,RB27I1UI~ee=z;vk}ܯ~@HH쳑Qaj( "RHfDdqCL lr/y$kl <.BLg9KV  X-@+I4pmӠ,j-cpqE](z{vt,a<~]N@K{^?5Us @52h>@X W_W8 hlJD3.h_0S:6h KܫO뱖%=#2qOռLyMIPvN2^*r[/(ޮ*mvʜWW(Z=*οad/`^@`=G#sJ|6‹#;$a :#;#qc[gL1yt/bW5צkt3u3٭Qv?JNgqw Z#}eC#z?S.KkC^ǜ?u"A z޺K?9t!:- Lem$Ek(RyX`fUR?Wwn~P͊~ )"dc$woxY~ endstream endobj 536 0 obj << /Length 2241 /Filter /FlateDecode >> stream x\[۶~_4caqx$iߒnj%R;xDJ>DbAᜃpFo~ChG7PIi~8a!&E,K_y{hͻo~B!0%0wDcQkQ\_ϣ]tГF(b%0$ȿ@'YM2b18]e {\%Cϲڕw$яYɫfŒ(^EFRk.G$’ĸ<-u16Zp;YD9(beB nhҸ+R|,\H[1cyElk@!SC(R[܍TX񻠥Z+6aKSVOP[ ] LGѸWh,zl]D׽~E#MmE'uCமWRAPbZIBExkH$FHaMsJ_ uf}te@b}}qN,{YޏeG gBPI, 0)+Wuj(zL+5zђzu;|WzM;J'^D,qKΓC#;QY󥦨w4z_C2{MZWA(ƨ9 ԞI5՜oiPV@+P yƸ!؋#drڅ/{җ7tu_@3P$Kt'`Z]Ru7q| /P_YT:i!ʕ?i=SDQysHؾ*f4I,l;{^Eht С-bw}v΅i/ m^N"NG"F]b5a;gX<B',7NeNB!8oB Lߊ՟s.Ǩ;נ8Kqg翭f5&;|ѩJa>N}/m$IRQ %br4 ]ԉjmAvm) 0ݗҁGD/I߅&|߅xWIYo:a_3%\O.'Zf%=6~ ސ!_ڦP"Yc-cPK"y\/{;F֯mG{Ѱ GtrW=@l1߼kd`hdGP+PЙ>mt`i/P K:lCԂWtyt9I'{KZ*Ϛ%@e# Ijuޛ7pȥy_-&/KYu,ue@-ܪRu2qxһ' EB5Nɮ j=l;GADwI^c-(f xrkmNavM}*ՒzMWt 39@ɉ>aYN?+O ˊҷfጿ@Lrz7_~ݦ>zxГCMI 3YWV8H3%/fr3Gx緜G_QXF@Cp{ѣI 4$Iu˒zm*Em*Z͞oMf,Χ،]A@#BhTd/\}ȵ>y{eXV]5NQ֬R/e@_ۡWkѺG!ciutzeGLG\E9i{31pGÆQndVzzDolg8j < ofg혰~V=-Ne_&CCrzWQ ʈ@#rӱJ^4#NL7q:)^Oo!Һf_AJ endstream endobj 540 0 obj << /Length 2393 /Filter /FlateDecode >> stream x[KsFW`+9Udj/u\-T 9  ZVU~<DQM)Y `gzo/^_^ AbIep` 8B,b*79UQb꼞Eir:I]"廋7\ $PzH*Xn/>~ X76 $g.gXƁBİ)y$F(Xei+`Kp][+sZ+OI EC3y;,"T8?Oe$a0RNq]Qq C"%UY8\B,XK Ac}Cv ~ Xm {1++Edh *j8SFܹ&dA(A2lKЍtVO3^}@ q Ww@?"UG( sӾ?cMҫL1m*hs3s RDaDHWw@q<@yHh_ $yrS8~\$%ZuUee&F;DRf|&UiPU`3byH\&}4;\;=띾k}8=uZVh_G NުCON!f {}1RJ$OJ pr7ޓSv/&7žvRK}~g}=:jy⪃U;i[JWX-H| gؘ6'C1]qXtuy@Hw6iK𜧹cΒ῱Ѿ<1`S^ƿΆ>4gzPʁcg 1GO>2s 1Jװ7 &g _p/ӤROjR'! L0" EQwygZ9y XU}ht^꺾tᱟ|eޓmhSm~_hp:U`%IpJWWE'/NYg/=*"(Z~7  947IO,3&/3d˴I $\&<6߫E*א9絟/ڽr_a:jzqXdiFٸO`(oZtu`;fzԆf _e7 vH1F2N?vu"q-v*I?ݗ̑L k+-"= )93;h-9ɖ ¤R1˒t>VCv.P-7Ne]v &X; FƤ$ݭ%m]WM7s'~ LjQڙf"?4PrQ]H&-jwYMζ7W\y_nOW]nj[/՞5֜kjeKج%6Y=>`ߘu`wp4&~q%텒1L"D՚gP /ubcX9f͋Ing6KR$蟃-0Ph{?L< ϽM(2e5Wf:meҷ:SvJ f[a &6^Zb7qץ'lVP̾5JaRZκ|v@o=On;,@#8?c0^>[Vzm'`RJ1̹Y]&[6Sp^3{h/b.Cb+rYljԃCސ<򮋴j/iJ8 FS∋ڕ}vIXr̢i7iӗKŅnk%]Zme7 a^!:Gؤƃ6+ا  (#&wҽem-XZ69g_6)\ F'OFMS DVO?dfv67r D 䑦̿9gž< {'C죿%d=={?/7>ݏ03ćEUC??Q߈t)"01K0sTKmlf#!aG|5p?\Y endstream endobj 544 0 obj << /Length 1788 /Filter /FlateDecode >> stream xZMs6W7L69%MjTDʎg @TZԭ@b_8dZ*%%-(,Uh)Gyr%:u@PGP.D6C u<>O~(>OI|D>_O W'xEl[>ɾ;}>C7}P=tvžXb_zP=۾X}M&]nI A'%˹C;'ڹޒoq$Q]^Jۻh9pd-L߃v>5"vF\7; {֞8ˁe+aؕ,`;%&ܖ: W싓ǮȌ_>&|v9;O=7|IGȝg_+/ åLʴ^Q~lv/tcI>٨p㎳b<2~^'N]k^xt!Js[1q9dg$AWxċb7/2.ͬ5O NSW1voZ̫>S8[Xwj]ߚqRiVӞ-ZNiPI'xjP ݨz_EkGX'@IN(L$_^OO+퀰׏܁n+څvtDe>O.\/zpe+?zmBʁ.[$![{Tou3T-]ά{qbgM21@H?D:PyWǛ;pTM{LW朮Zyjq;@ژT|vbVðWD;T-Рfa[43tݘf8.LiӍ=Q]{KC<ɾ^sO5KK_.3*8rOFW~jCz 8 h~ Cɣup+?Β񈽫` Kz endstream endobj 548 0 obj << /Length 2648 /Filter /FlateDecode >> stream x[[s6~z F0$Sli:۬f<4E[%Q8{^E_JX"Q֣/O~ I.o<Jx~rǟev,D<͖/MdD!v1zzjb:1.`rq2WnՃ݊׶4vNtHٲ};Q}<϶к5<8O?6w!$~vQ/g"0zZⳑJU(PQzͱ\tX=V@ӖgFdDyկ PQNp>; (dD˩>ga݌xq)ZH6d<Z>uØ$uXd.BH6vST -Ym,1, 5X܌%einꅍJT#[,I8@\޺̼IM2Z%'6JpOcA!ԃoLjeLP<ߦ6YG;j֝bosLPb=L-CJT_oolDXQCߙ~UVüjx/>VaJ;Y-'NT@^ߍV@-** ٕޕha2M#h+ph/Fi;9bn#l:'͓uiyi2c[E^wiFݢ(߮Xݍ9jQ\2ܤ2tF@֜u>Mcx";%fYۍo_]&A4z,y-Q:u#<iL0*])Z DyiEk `2=YdkƴJnWy?2}@61b? :,݊v2, װQOt|ڙoVdٕSIjۜ^tuo6-$ ~yEG^k0d}+VR OyS(UBG1Б"+` aDT6i!zI(`%90 RSNK/Fh(q̯R⒓:|KtJQ,i݌0_ 1Ycƺ8vjeM68Ipإd'G@Iel>,L>=$tcl:"mU^ģO y/RJ7ӽkLX:y:>;'YT<CVMBEuN"s__>~98`ZF /1hECHc*_a AtY á\k}I&=JB?\[GpM]s>ˆ';sӮGS"xKzNfC*d/H㓠c%[ևBOv(xI?dSƣ<@uݞ[qET1:nF@WQ׸YTZ3` (Q`ࡗ\6^Rzͅm{~f=bH/Q~`j^0gڻ L{?KoswSWaت7dmq{}yǝ?uQsA7O>G\=QeJaɁ[^_Kb2DP#),qV endstream endobj 552 0 obj << /Length 3442 /Filter /FlateDecode >> stream x\[s6~z2ҮK7ng{I'}贩d24E[l$R8=!e[l 9 `:G_=}.(&q(0l6z5ׯLy-2+T:/S2[dINB&o~8#T舙^%Qq8JG Q"ht[-GR1ʋ/G?Q%%+G/wLR$9>1,iֹM#aY HQ*"金FU!FiӐ1lkcqSΨm%Kݢ!Nil w^+B_Hҋ#ЦXbe%F7( Pz7`moA[ :$ƢQoj@ >̲sm1 Ɩ 4$!qSJB% ~1ʑ8cÌW<<}-$HrY5i%0GjZ^MWUNl\[%2Sv)C?^%|"K{zSIx0-:"!K7[]3˖Mb^$60aA>Yl':@ztYC1= ?.ZкHHHxGpc 쭎1l Vۦ楓(j'”^}6"v#U]tp`ZiS;0/<ً M>&cШkbx(fN$` I"!3yo9:8,C$/{种,!>xڮZ)!$l:N t"Ы[{(_(rMd87(l4T|3L?1wCD؟F' *6"@ԣrt؊ҠKkǬ,lyg-Vj~QLFzB6ja8Y2'%VǟURTҦ M[ΖL[l~$":C "KfPi8u0jWȲd0t.`PN:Gjخvr~{+Wc܄Fd*pFӔV"0ƗA0ϓ52uiitHXDXzk&-(aVC$,@q (4$F]eUes;7[ܡ1'5rC q~qc` VAjmdl,ҽh H{ IZjZû *jSV묂Ѭ b<7EDJeH4HW[NҔ t(5C_E,6*m#3*P9HRL3kbg5! 靰aʋV&Nly-_@%d`SYNːQRaQx$d.iayMvЉ8V}rݭ"7LF}IYgyc\Ĕm+Γ)xգV6]n'R8b CsɢLJ`6Ec/@J2v谒hbZ!+WbTMIU9$b’j5םxDMYE2m- FV&IױrV3+MZ>2K6m532SsC;skd[X٘PXSn)NR2b.㝧6WN7˒*_ 7\e^{s5/f61fc\s fû3f'f>%p]n#6s=hCeKf.cG-`<󞱹rJg#K}(3mL1Mьt$ϫl7KNT{:wI$nz7i e*qXcM~YjMȀ 4fAB'poBFeVٛKAX1Ì ؍4;6yK6s7 \~q G ),Ԗ5N5rU.(xȼlZvZ./>NFNWD6@`*I!in ޷tvo \Mc"°j{ߝuņ1>nuD[N 9duFSr-A-TpAKӤ<-FF ㉢́Q#ƣ ܍BYιP|e`Wc7^~^xqrwz=6UYzN={>rv-c|.s,yL$]&)uAST}G>~T8{w?ݹ{kTw?V6A98ck=`h[}ӗҏng"0~tS !ƿg!;:rQGvJ ;%` n&bNxGP'A_ޏI2Z'mo0'=uro "i7rd]wt0-A"^p{ jU4-:/6Y-Tn~zע$<SV;zr> stream x\o6_1oJŶEݦOE8J֖RY^pC ĖHiiJfH8: B2 ` (D,duïp:*<>oVIZI\,LINL"o~8@W8 VDo8F, /eUATI_|-! E M2灒 ɟ,-&>ɳϓS*NZdouMnGo^]q-C$ 8HFZf^q DJ0,E8R?1hvIpaܾ+.u|#%/k ѿQo Y6Lز*]-Ud0+,5ϕsNZ%4ZOQ^ݩiu8ąi@jcBR,|wE,T8H֩)`E""W7^_ [ꏽ{ EnҚoy#7#8!Ptaǎ1v`D؍9pa\Y]%%ppf\Cp?EZ]D[ OW[Z?BfY*"&Kӎ#ֹstF*|czඐlsG['c]$c:#eEri}oC"ĔP1Lo 6A.<^]>| aH$`] {^Z. Bc8Л8ٖq 9b8UMz@vd=8$c}(\2pP9(&E2štլg8-WA>᎖q|%!j|wX0ʰ6sm=ޝkD=M`2>8H:"7?boN,z,beޤs2m^k\DUchO촜$.)rUaS6"^ucYkS8lcy-c+)|q)6,{]t/%6 an:"hF|w_ (qF <:,L y!I_ aib,oHټܘ5,K*a|M?pr[b<ӯBT ̇M0[܎ڰA+VNB;o0k&P -Ȝ3])2κ݈lġq/Ub'>4\cb&goF"ۀ*Iw8c@cM̹%y5&]`L3nV|KN =ī};I\˪A:L>[!: wƪr"'*ш ba+F:IL3efUF)v0/X.brNy-R$ K6'mt2RRo,'Ϳ>Jkt#ܒL.`1o%ި6h''1lSo{v\ӽ,2ߤ] %?p8Ve=Gs\˞d]xJGI'nv%ZrG(>J!ej ::28B_>'wr<;X5z{y(䭶UMҞּ5Z:p&Kɾyٻ,%3zM}a !(eHJBD؅ѡp3=ٵN0rqpNecf}W]/UonN@O@v׋N^mŋk.B0k_lWNM7VI}[}˨g\q94\\C88 ;K!_/5g^>_=wvr0{N؝2fϢ$J#) ]~zRԁtIQW:/}~&Sd۱;WŃ1s]i<:2hz?$,i~Dk2qbWI^Wyx @Dc(t |/ױLUpF!3? endstream endobj 561 0 obj << /Length 3235 /Filter /FlateDecode >> stream x[[۶~_ ƍٙfImO2I8CpEjŚ"n&d!sz^]}z}쥐^DSSQHD(nŷ/ g-MZ4?,4O:5Nazq}.cWIHyw?P/%" Ɠ>#\PνoVJHK=E"EJ+9ɥJ|S.ˢɊ]X>rcJ*s.+wķHI3#yǖ0?[ЫׯPV $^@A`ޏؒfr k@6C" &ȾaDH-}J$|L%aCcܑǫΆCt} Y5 TZ;%lоi[Z_k] &sD!=hFecq^B92YD4wL `ʳDgJ]8A{ꇼmgl:o!Axy `4}+``T`С re2cx .ф$Ey-|qwӞ!0!!ڑRfM|d$踂wr9p>]0nP6 nX8v_V),Dŭij:gEרYw/ݦMIܤQ87۸y26c+kWKYX%-xcKO̔Y=njM>kY1Sm]<{jwt>YeyzGTͮi۔Yoek=>& wuݔU|̄~P^OK\V賓#r4ǘqF,X3`OM0߷duK4,` U^.546[}?cD^<;u9kSA R V&}G!E7u4@Nbz-ug'S>{߾>_?B(?B08¥=_X1t\wQ ԅC>m)7 0mo8 6V?S$'2G_5%% 26p!#`lc|8쀽\ F%;JjR\x(fITSl f_ܔnR^#!b\j.r.ci7.b6EdtC$Rpmy61 f"6{ΖsF2^ +XVU*K=U+Bt7WX1$6- iau$ Fj7&u(U3"*?}G`bn2]ϳ#a>-ǎBhoX)߬\hM \tD?*C{#4Ĺ&|_ <:_USVTpvjWL#Ms'xa6TD-QmGDdW} ~~OB6qR'rE, "CǖbH:K95VY֐W*ߚ؇psGW62MT'puV.GuIԺluYIYI|RkA  "ɡTVSDa&G  C*%?/ 6CUlc1(vjH"́f\OKa|X$ɢw$;IH #IHӛl܋/Bptե_[bU e}{x}6ୋ4cetؗäimh0eSl@KVy]ܦ۷Bh2YlM-#xk-}F{ ߈|:Cs6%pgsl9s>(N>Vu©T) ' X S, 0*!"P0Z͖\uIYh]͒JEw:0i:R4Ûkc;\[ O;):ɞjIX{*d&DSSOpi0z529_fzZ/fAK@pDb/¾rȉ;=} :#:mL!+F7N#? 6#u9 05Z[#f2ub!I[G)`޸.O ˰˽cçٹ[øa $݁ǿv7`Wb<*`GhU-Y 4ܹ5ESKgoCg܌䎃lp@ƬԎ){CPXpa3 ea~7UGqV>]3j`ˆދ jԶ=Mv6}ETHDFƻytuA:α42<2 d/ f8D 0]bMdai#Z &lxqHw14`0cQ酈_2ԇ{ ,WY8-l/e4&#ƃ遃LG~ldkueE7xmJ-8I2|Eg{ Z %ToSK,xg n;XȹJY8[fA%hP6ؒZCO ,P94KyA夈7i6U SoCna> stream xks~#pOۙݸʟ$` 5 ۓq $L)cr{<{uK.D)%'Lr/BC]%70 c.Ӣ$UqaN4ޤ惑黫g?\}:Q/`Uby% B􄤄 sg?*“̖~g#t}R*|e\-TNnL{:reJI)#+̳IKW\o{L|can:Orr(W83l'Ɔ6O Թ]?N4+^X4](%f 4eH:ջ%$lUUj.WD ?vI.E\5UiXBn7];LƬ¡[+MC4OkPITD1vOo@0w&|0p.ZHLZǗv^搎@X3vΌ$VlL_SGZ}4oYQVJs2Q8ku͛ڢnhrlTo' GT+Aw4{kFӥkXcF_Eo .bn Xא0S+jV7jZSAP f<4'ǁa\/HKSÜZꃳR/xI eQ_%:"gyV}5­%:⅗5ZZ\3agq/W[-,cbY[Eb+ƥaZyiEǼ -V_ڮguh&o`,E*5n.յ!Fk˥ΕdUwJ[ _MeEg`EV u2vTH)tOpUhzǭH0~@%&~ pL6po$ "\Q(_֌lme&,/fyz~".1νPv@&[hoCȻ( j%Rǡ /)֎Ϛ3$~"4" BFH)<>Q+>R&¢ hoB*SՊw^f,i%4b x~,<Ů;Т9cj<"+i`hpwx'xѿ8ۙ(/#lzр zx_2}+4{4>ˏr`rp(r>Ob9Bi3Y8گli>JV_eF, @dą5kX~rWe73P܏=iG=FܓfO᤿9qE]a\/Մ8?hgcg3KXet\D1B_D/{N=I$%dgc/1'QF6ATzj<{w|{/()p¹/>l΂*AY0?!Gw"mƪۯ&b6~gٓ,NYD'Yݜ^v.:]4:g0BEUsW΂_Ŵ9ox0zL(ڽnӾ:AF9v쀍9v>aCwcM&?D 78 df]Dw5(1N}`/ ָS#HT/qNJѓ ù)Ay=b qh<[6msD&RtBHSq*mXrz:qcXn)`_`}o_{gS.[${<->>aBu/qPt[!7+OJ~U~\"ӎ:7\?`wN})?3l`3SA]Q*FҌ3utZ7`MZ-oAwS# e_h[_t9]6b`>pһz`%mЮ#UY֡7hГb0kmI5- ƾ5K_1L_@$m8:y\ڔbsKc@38AqJ$\ZD}%a 0X*z˸y]ݯǛ`ݻ.S0 4dy" 1= mHtl@VnܠWڡ..K%t%>[J[se>MJ5 @i+k-BVq( ) _e``Fyϝ`Y[@45v0Ua9,ĻcHtuwo8o'8XQ#"#_߽f endstream endobj 571 0 obj << /Length 2637 /Filter /FlateDecode >> stream xْ6};jw>6ImvTIhi#Q6E$J#ÈFw/4]z=AFRN"E#e4bEUo f.PI>˖7e:Ou)D޾+8 կ8C#ft * ?W?]ae ~)I[/kGKZ&bL9"@,ѫ8zǛl-5`9T##Cv`pT@N>V2~;(<-OV"ӕk% .Eʀ BP*))6q2נlBdkו2Po"ѐ`p0`(Y7]0p(dY 05ϻE6zw.a&w+ !ڿ^r^ "5E+t$5vVݰVsTZÅFri[\z1`ѨѢ +T L(ԏFni.ӎ\~pAISeJJ""@ L, mW\m4=]S P]Oa;_ / ˊ>eoʡ49ֈhi0 h BBpPyPK -92Ly)m?e(KkJl oo 5C:0ēZqx͛4t{ǘy|\éVuoEE!8X]AOCo@XB#jM$|x]וzO fHf>ɧ/8zY^,]%+v>,wLrukH ?1qc-xlQDӧЬlqQR8V\{"SʐRN! I3>R .7Qnk>9dnt5""~Ȇb@9 ޖh9;|a[`fY ʪ5Pj[XSVʘ~K޶,O9IߝW +A9ްWDD5ښ'M25a,?7aa=l1)no>Kઐc\wܗVRev|!*]oǵa{GvG؅,#˰,&vf>  jg,?j_zFckhFͬ>s*u<^T ȮN37LtnƻldY>uh$+ _F͸.!ܓ5R8i(rӗ+n ĘF/>&0;XWP&BZ(CT ;dF">O ]268NEEyYYpIy7qS$o֜! Xv|UicQvZ2Xx*r5N!kP9l_Ӥ)rW1GMOK[^oMnH%6 endstream endobj 575 0 obj << /Length 868 /Filter /FlateDecode >> stream xڕVKs0WhBO?t( TzPlv׳z9chzIj%ݮ7@%.3q1eyѪFۻ$eyón$qzՠ݆quݭWQ3*,sT=G G@3bgޡ/ѧA%'P= [;73M~۹[:YV')eImNۛNI>hC pJ,PZak*5SȢ9aA~Wzajϔ82}. S9yExnUm.¿t IwaU<[3֖Gs'obw~m+5X@W'YeU)Sʸ6n*ՐP,K2hpKl Q28Z*3h ̌|oOC ,y8Bg4?e;׬nMV7 zw?(;; !!%9Ay cR8 -Xj)sgRY0#qƋNUKxg(<9 |2կj륹,_;68撿)xOT9Ä -nt'SM!`ԃ3vG#  cWc|n'yjay1m:Ǒfۉ\gl8Tfbl(3= a:h%G 2vQƉU}w ^WIܵ9\`:H=ohKfBtȌvх«]YA+ 4mz\@XlN )0^qqfÔ2ëAD7!c nk?W<_hm endstream endobj 461 0 obj << /Type /ObjStm /N 100 /First 877 /Length 1678 /Filter /FlateDecode >> stream xZo7 ~_u:QEj ؀mAhevh4>_\.{pNwQO$K.W2S\Jr1\٥]2 2ŕ~ NI*~YkG ;J(XX <)*誎$&YZMF\doe4#b ֥.f{F3رZh@C<jTtc%LB]ekT(dǁ0PJ1;jX$2U4Gr9q!<0M8@%MA]L0.<`g4 *$b^L l.002u@". & hj 6 &Le$:.bz #6Věb Xz*k h\xH3b:hē ^CC\=(#(-Z`/lldk) !a h3x,YlN e`hT4f\P1FC k2Lф N-Fgحbh*6ֆ R2ۛ4aTro9`T0prf9_%/ޞk,7q[.o\C.{ nĢ E~) yy8:-ݡk^>w٧bϧ3tL&3X7/ύy5;_|8;/ӧObW䵾h3ZX`nDZ>(l~]|d>͓|@m&m4\~?k<]V#7O͋!n̦#L&Qj6X bH>'+\EߝOa=ٽ^ÍQ.nTt$K;t)!H ZÄor>u3@ !|(G?!.F$ -9\f=ٷcm^^3ic׌1.!aG r{ق9G5ik/VGpEbrRuʻq jyKֱ] (-^>z-zrsʲEjWDFAQǭ-x[˰arO]u}UkhmqDQX{X[8bm׋0 .TcSttv\) EF/>B u赓]ʔ|S Ɉv"2bޱB~k?"1gשuuz(l.Vvv*c~j! 0m~!u;=4FiKrp+;-V tAcA]h'ouQ/\OSq;+W@|C{I>r-d4mrB!R)q: |߇7 㳃(Yұ{?ܹ&ǑU|8) eu\Գ9J둙P[p ,ZxhQmVr7nEx`[p I $kZ}w/>>oVG Lg_/sɄ^>]֊Ryy횃W/ݻ?dzrng8{<)v2(XճpN?|<]N͸G򭱭%MBHK˲]ҳrS(m w4|3r endstream endobj 580 0 obj << /Length 223 /Filter /FlateDecode >> stream xڕPN1SuI m*Ja~bћn:Ս๷Bx'VA`K5ҵzS3k yS-$w\]&7"QQ57A?a(+ikmΖ|rG93K/?jɅUA(K|ebP)=dǹOCcfr$̹ {R endstream endobj 585 0 obj << /Length 2444 /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 @{ endstream endobj 591 0 obj << /Length 2373 /Filter /FlateDecode >> stream x[[o7~5 -,׹v 6oI]! Dٳfi9$*ɵ$g.Rϐ"y.<<#zW|qtVH/"}b PxcSo? ;f:Ջ$KOlG=R'a~8rĀ Uhv3PޣDDwkZ<SףGqIR/ Q@r+9Υ(X4Oҕ[&lf 5VKW_an,a\k?W޶>C Lx>ZFjImVq4SO?C B"$U~aDHk :ڛe=!9z>ʎJ Z+Pe# >"le(~a!cV*K `ԨiJ۶QRepCXm3&dv}k-ecHc &\Yy29`A ~B, 6o7ԽS yso')(GLm5MʊD-4-x>]6p6:]8˯٤>$"\,1F?j ӥz8ޕS[=Qdx5;$.il6L4_Mq.ڥyr\NuaO)p}vrY)`1cR kW,0+ ӂbYChEoUb%|Tv*lK 4Sd5>[NǪࠜ|DZkqTym#Sd"^Bn$z8׶Qq=,D YO~l!oO"0f2qh6\ţBc{ ݚ"V:=rgKWbm sOɕj) '7N%{]`LvhCpcE7FZxГWҡWU6(v_/0;]M)mrV>hg %:5¸%@+xs'ހCQD;(Ey8z)8Cfakq4U#rYi0cB T$ɠ$@2h~ 7daGqzy{!hCTk !Y~/aԕ-T.gBDzʵcg-e|]l↹vi,"Dk)Д,9P5€$a TGΑZB>ͨwq8Gqj_.g,uo(FqH6MzLPF0a#R[!غ(X\A;KdNJۙZm⸷$ljs0o5M![}M8Όm_c)].I*d;Uiv}~~9]]ŕI|9JKm清m~E_7Ǒͩ|Y Y]`9"`Eoao/},Q8sW1| e(TN"onuTC5TVQ9 b%|wEmW_ɝV@B%+>/.?û-rT"?\^HG7l~kaV 3!*&L0\Rvd+yɈHOΦ~@ i@."O&$"n.иMSvsGݾ*heKwG]_C+be`, -1&edP2&#l7kQ6#V"k`^@ HbzdVo4KTH"[:Z Ns=?iA_ @V۾'9 g&XNFf#Z6.~uӡzad--Ӣ- &k>v2 /t"Qj.łxh$8xoED{ǐ)OE-:c I\/x7WWgは'ȝWil d_, U>):Y> stream xkomW.gM\Q\=$h E*"(;\>ؑ8)p3rOG?=^@ɹ|Ť0w^^ ٟtfgIWc̊,3H21wGB jǡ^ν%_x8.ͨ}d@^z- ?^D,@Ly00F5anE)`aFAJR~lZ@yiD1|T ޒ=(5]hwtޭǻR( mޯHO ᣓ Q ~'ǁ‘"HOA̸'_#䵱-@^GϤB2d"Ӻ6iOc%X`Sc&Y̭0"_.,i2&2nyYˌWsgnkp`\3;m^^e)4Y ۝lŸ]́t}-ʝ]Ӣ+fgv:J0%.lu' 6ꊈ/r+Q oi pz>4 YvЛ] ?@m36_(%g*ʶZ>8MJL @J:)Mk5"tKzg niV׶m /[[V-nBAzÕ33C%ǀ:,z+ΡծszkH"e/R0e ͂]6ɂDcDqP!sKM_,$aWDhZ B6* 2d҉`g;Nߓəs) -Ī\:Lԧ# C#|$[3/7,h'ڭ|95"7r v2`/ `d{hj5«㡺]|n yCvBpP 2Ǽ x#+FC99H74H=H8j> g/V`Q1YM90tKH|w`f0:l.wdŅu| a^Zxf=R֮Y Y[i) w"6߅;d R]jm[ (M=F4" um,kYdʼn_2;pQ2ϰgh,5h~l}:L$jM7`J/!*J?9w_'_ӻ>E'Wɺ8uW(! %*}9i;U| *@KT"ł(bD> stream xkoF/)P A K;MQia2 F~3;˧iٖ,Ź>ffYMu3š Wg#": ^~Ñ_jΊ b2RQIRE2W?<}u@*<4f&xzBOg*4F0iC(g/eR:!gFXwnx>+&Uz `>8[ΧTZ,ӡ4*-lIaA^-˄4:7U~@KÈBV,17FbkN:4P!"Gm"MfFIQς;6Hy!q- =R[EDN8EM߄[j^0W jrs0mo&(5L;,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׈+֒ endstream endobj 605 0 obj << /Length 2123 /Filter /FlateDecode >> stream xَ6}B蓌 PȢm<h}jVBdɵn;CRl`C14xٳE$V\@HA Fo?\*ݮe4yU~௲"KU8фxy_g Hрè`%"6 " E/gԳJG,@XS,GQ%iTҴ*f `KpVd3.ÿj[{H!9;qyjznV(&3CɃ#M*^hꈤ!"FD$ tPSDK%~,TK:~^ts{+OºбB#me0މN_QʳWNUa" EH$]U e @lya{F0SͳLwud7O8g &\z#rG|GPqP7 ۇ/ :̆ 6gDh'e^dcى{5/m_Gvnʾy hMi8ACzhe@&?fGt ?z[S9 :]) 1&5Lo:u4٦;yHXIuvN.]o04DG&/ W5`,-g okWj2G&/:9oח(?v@F$h8c\V)2j쀛n׳;a\:|>db9N05n;íܠI鱛}'체YZ)!&WNo&bu[=K0Ȭ_kݶ2!Odϱ_mO%/9&#hMXg;oQ{T{zp}<DҞjT~'.'z]])Ӎ2u3ҊՇDuUrj߇8! =W(%'v6LVGH <2јacnٍ:c<=D0Ene# 3X1D9@;O':6Iz. (I(4|m킃U&K׶mHѦP(N ^ |KFX{ZYʪ x^$W'/M'}ԙg>ummm\)$]iF&qh#5==(4)vW::/_$Vuysy7Q>*0֣,[ ɎҸ Oo`7‘ׁ T#".U6rc}[;  t]I&, ')S88I_f 3K0 aS-mis|-ֱ`=GC}_>F=oJP2O^u {! m.}@EXdLX (Lwл5 zv^ ilw/V5 #abƭ]>YݱXܼYcѕb谯Svpƭ*/=x>SUU1_yoG܇x:NWu*)IoAjGZZEmvbƒtxdZmVM?>V@UkaBÔ2^haӅB]hҙ؊BOZG]xqa|Gƥ'ܾm83k19T Wq(` endstream endobj 610 0 obj << /Length 2694 /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:ߡcM|OTӅ5t rm~n A7ssr.HroD.iRWh`AC %;sZ@vfTܫhebU};r>Br{C.Q@=Fwz8[w !׀)-=`yܶ OX1 O*FgKmr5`nA7J_ʵ]M玦Vg PfoۍDH*m, QD8&VtN06w_un`:P_%g? >H4W()kjuTmNs-Eȳ6Roa e"I Mב3z>rUux(if8c(Ţr׍|P c./D$hTg"oֻrc.c.b. ᛤ2t1 /\q0>!@^/ex"a,qOwYmLS[)]v̤ ĺ߉"qa$gocN (m̉ζNQtL";&U?컿-w"FEߏIi8$"!ߡȥW0UWjHq{O,G9^$rīQX~ǡIաHP %23N jvFB뀉XL#͗!;@Oq-A);>hl\U'j‰s[T9,֯ƧMΎvw)$g~q B?A$Thk!yݨ?U;-Pb̙C9C s[m:(a?K)%\ʻ'GoL,:HB7=qO@r<#]Dco]@$-][``DJ,4)Up;.j'vOVY*+ORwHDb*n2Kon3ʦ/s%6-! SD]KY7U[HHEv׹|mjY3 jY]L]ҵ-VtQ P$Ÿ%Vi{eyf\m  0U cnK=B1eURd[[Cz]bVԴJj/8HjyUI*)2SQEhLP.j\6Yjrb<OM] SВ*nxmn'h贅cçԶgm/V+'[e:煩ll`Ɇ,;дDi0TH툝 vӗܿWzvaf Z";0F,гm,s=hYZvrEWrJ+ߘ+OUB@6OEs^,+6eM<{nus g8+uEew:ݭ0{-TPr^n͢;Gh&zVڜ^zuȂ(I/xpP ~HGC٣k@ =Kxw t qˡ[7ftNq'b&ms4+zɖV}`8++{LCմb 5Vz=-O \=3.d݃9f_j{ Ūo;.)/&^tO̾+vPZ,/ڎ)U`e6&xFI (jhEO@wU?d*ٯ}zQYQA3~BG=o!~1?6ɨ݌B endstream endobj 614 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&x84LXz endstream endobj 620 0 obj << /Length 1868 /Filter /FlateDecode >> stream xZmo6_2Pq(RҊ hk&k+Pe9&K$7˿Q|ыv")^`ԳBr[廖ԚͭK'S׫$:-'r"ɒJdE>"ӫliB !V\^ak-hXͬ1\C;~`%y"Bbr*VrbbcՋxzՊ1QKr9gb]: c\=BwŖ(dd{1u +a;`c@< ?'EX, !=, ʢjW8;x,@qXjŝYo~!ɹ4C14Ls4#[~1t(zzFzkLXx4V! 6 "w=+-՗` eg!yK.B}H+ 砞BdCݤ0P1Y> 򫍓y*[yP#8UBxq,t)b)Nx~rv2@]])]! I| FB-uWYOA_Wײ1{Rֽb*RL.L [-igkxuJWgGO>lM@w3ViZΌW7'g0sDZxc _:=Qoۦx?p(ZGاAz"žvF'Z'rFVd7pXjYL)Ӂ7.V(NCiba(miB[UT9G!uxm6LDzq'%\I7Lx$ϨuY%EZr72]-*9Cƞ8 %aQPl#"|CQGXGnv1Ӡi~f-S5K,4u( ߦYAO਎>2#vqzILJFОBAľҍ]6b}j;ಳ*@jjܔER|Jua0| پkY+reuaT&Ӄ{Ϯәih`BGWFpubMm,,rqsqSI1$C N\Y4Rt0ȠiͽL0WITJ7M:T{ I3dϠw 9C0O1rs| W$TzFY JS-}]C1y ?/ðU9!WGea<>bH-xۥ (O~G`. @֧{o(lT[Wc {cҿ<&źnvbb<-V ]{$)=EJug"na,rاuC-[ok[fK;lN|⃀`POs!ͺ~\Ơ}s:7x˘w.Ҏޖ.L)LE:٥:M$#e[;lBX&?P&l 69,`kDtLҌn}B7N  I҇DB5 )T?ÆQ2,A_QN._)[ǯ.Nf﷖ _L7&UA2FC\|EC;OI7Ak^W\Sb1Y(EYdKfsHөCqhϓE3jO;L"Ia+ LH4HEAHfB ̤&\6S{`QM7dc|zNb{3Ox."DB(2Rw?Dip/E5\+| CԘ endstream endobj 625 0 obj << /Length 1929 /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 & *}?/8F endstream endobj 630 0 obj << /Length 1990 /Filter /FlateDecode >> stream xY[s6~`ߤ}rm%MꦩLz24 [PJRq5=&J1C~<8ù[^|<eVB;Z&sEo<;OxTq5qvd<)"*Cn[[ 0a`=]k9̺>k.q[pv)u6'8[NՎ `%KHapHM ]WW>Ω;oێgw<ĝQA-zLx+z&PRu^>'|n}R}vHDU!8~mfYT I56oF,xlxbDp"q$ $QGIv)}0=i (Fhg~T{zG9` Ɣe WoΛ0MT׼+^W#>r)֣Ӗ(]J!Rd\*<X)Jj9fy?hDV`O"3Q8@*6%i\}}[ j'tKˈ0K( s K s324 JZG_<㠖zAwՆǩ@86P #yUrܦΖce&ИI%WE 0)A+y l?hU8uM?A sb?PCq9|jqrO }{+Ԋx><] QM>7d>bha/K.S; 5ƺ]\<1pkS(tSLhʚ\@TΖF_U{ʑeu/N_,Gbt yZ*zS)`o( bXmLEu>`n 3 :.! BE@;$g6K1l&YsY-=ܞ fR@{ JO, #R %s/ z+$(s2 sZ0VtOОwҿB &+L+G#f @7>խ|0EETM,^.ޚ<6>?_dz&`ﭠWbDj }CTIeB*qow7nFЄ[GX(N)u~^l7hІ{6_ҋ3.ۺֲ'G1QJۏzaLG:y0Z b1Zt+\bQ9,D ;bʭ4 +e &}O !0(fh3AR񃙋ybw2<>dv endstream endobj 634 0 obj << /Length 1514 /Filter /FlateDecode >> stream xrHUhhSVAr-?of$˲X!Zs`^=n :/o@3ɑ6!!Ge/`:쾞ǛY{Ft=sdD-֘wA[)( ,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,??Dj6 endstream endobj 638 0 obj << /Length 1435 /Filter /FlateDecode >> stream xZ[sF~ׯff͓c;t:`FBI}ϲ `_>4`y͠-BJR :  3H[7gO=ċq=_wZWo\?Xg E/G*w͢q:ewʩ55v`9>!H a&Y1E$yѮ*in-7ׇDŽ#SZY`bkŐZ.CЖHX{E+#q "KnVXۦhe#4> 12wH1##.ř܎*`-~RHzLAK^?\|8>?h!U!Dt-'(=; X6sts1h/- _<q;fӧ*&vdIA8gD'hiffJ%WI?l~lh+y?z endstream endobj 642 0 obj << /Length 1305 /Filter /FlateDecode >> stream xYKs6W`2=3!7؎[8mRx:4IYj$Jx @$?Zd`\~X췄C] o>p 6)4!.9f#mbc.?#lgy #$ɼx?<)saXc /H3*4 A_"}%5CBR̴94 T6YV.h1vuc|czA1z Y-Ec\Eb#Vp)bERgd`c"  ztt.j-Ij%݇`Q%F̵Vcv^dˆd^OWoHPLҚuBAX"ohQN|t\%IM| =&s"O'unSx L&RےLN85v!XǦ&]3Iκ=e6D# ƆׇBArn#TX`~%\LYPWcM*jmWk < `7 s(Z w\(VS{c8Q)2:ĺ6)kؠ!pX!y/i 44_⓿C#(Ci/_$m$پW(}פ0^!ֹWXQ` d5N'3}JVX+UR2Ƃ e{򮛡8Jk͠ƠZJ{͚P :δ^!}d^ ҤwRG2ƺ}\4Q7 q]bX1+b7ޠzRz]Oa%F 7Pia/Y 9C[?k#e0 G `tEV( Nr/$M݊`jYT$3?}dz>\,a[ؐm05؎zlܓHYRޕygժogr;_/PyZY/츶`klT7t4 c5WhY6[y bQGpT|Q@2.Q(QUiSoR@2~ ?ZJںas?Fp(7!țY endstream endobj 647 0 obj << /Length 219 /Filter /FlateDecode >> stream xڕN0w?m㵥 ek1S!j TA>VT]|}dpxOdQi譴^IpFU+=iڰJ>LǙUvqE!vX. ֆl$"AW;8$o9Yo5_%Ft6<-ᗔ(jBjuJX~>sU jOSJq8s 乌_vU- endstream endobj 651 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 657 0 obj << /Length 2583 /Filter /FlateDecode >> stream x[IsFW Y:*d'(A&A˪ʏ+hɲF:mAzzo JE JbbrzLx:[eLIuԿLW(yѧ#Y9 (GAXWU6%r.?NE89ҹb&ēzS:4%ZWk;=fZz bIƶO_E\6Y_VWM šKKkAӲ-S'uetb Ќj(9È3aYuL]So̱4_M(,Vrk.r}EBpy‰4 ߐTU^­cWr ^tR7C4R3kz&>f `a-\VUYYK7Qpl <`&0"š#1)-1.`AmQ+#PΔ@׮2J|]/7y8PٱzxPX!P ^XQ$;l0͡- L\$,#\vME5{ia3IM+c؅ P},H08 `Qi X ˳ x(9>&_vaSxP8g)&zSKsm <4ƀ^g=xz]HKQͬk@۶cZ@Jo*h9H6+4z!(Ϻt LR' |R ,a3@:mKN*fc@2z(Y?^`$d_n|Et"< HgZBtdQ-.i G> Z$*& iUj;*VųXSb*9inMLƀ uZ=nBEW݊2n_ildpuו++"-xշz+ bK5 ]38e%80mDphrez"ڮzbPS9w=W: MCX$5fP}`ڎN{$^[`6VJ'i:O%DQr݁as;ҭiO0ڮߵgҷgtVə2tjhܝ>S1:ώHPliKdM;7QKi COzV{Zv'F⃓.ɫ;vLBo x*ˢ2#'߾x,j]6IK.+6bg2)㙡Aqc%q1=bZ3@=b*ɉ:mhQqgv8ss|k Ls 9evaP1;{ycv~akWvAKR7dp{5] 9`7I-R9X~0 O);.*.8ll,eY@q WxœxPB no깾vo+?aqA9ć(|L<%jߙxP]N-RS ކ0?H`;Qy?:tR: ä }MZL 7 1aE{M 6:_Tpw\E#GunCw9g>TKLUgOBV/\>BIp@$C`m LK\t-ǸX¦oFKw.r;꺑qksYfīs M[z`F#P&Pj*L}T_p޽CS4)uZS waJ+%؁)L.Ta"#'V>p4Y{!iө6KY~PBXʋMJKO ]̋ @b tV>zj)t$ `6Pȟޛ<Ӭ)>Ǜ#£_4|4pq=rDA?q а 1{q.*ɰ iA29)*58"S- endstream endobj 669 0 obj << /Length 3269 /Filter /FlateDecode >> stream xے۶}'m 3IfNyJ2WvYSFb{x%i:I pWlDqo/^M'Q.rlvi2:JLLGo7w+fîl+*m^.JB^/^t!8Q`D<ֻ~ BYEB_\ j<َAv06J @^؁GL`=ocqq]6ŮfMpeqVGW2OB_owDRͥ25G,Aii;Ctб^UlV뢡yG`rHcBH҂ rd:iå6@4l+׽a ݊c3OF@T7MpT[芑RXCe\.} j9](@$xtn_ngHI1 3V&W9MJԞO$B배 ('VNY oH#רRmW6 (Xq?jJt]dTfaȒ+d2VelڮB薞wYNá}BD֤Ih/8Wg+M\A8uռnJiv(,}ywLzyjE~*#Tlc|.B6~Q&Q@D^>5]U١Ad%л>z$Q,^,BhcH|.A9X 멀N=kP,e9Ybc4 V$Fralг2:ɞa䐁CD%G>,rīvAjH՟QyD4P67Yu1~uR]|LVP vi~,> ꐒ]>yAȌr A,`Q4UǦ$=RE$ˤ?$H 10Č>H욥j' 'Uyޱ%Vdjl 5w~s*yN_%VKۙ6R7{:׹<˷kad x")9–Ĵls4ՂM3A J,Jt3Rtwz b40-]} g5~0ykau#v0FD @AWA*s"3e)jk4"e$a/ZG1] 䡤x.C8+\g1n5d@+StT(9QӱqJ$J_ּZp6fd?wdd ztBX^;ysiu.OH-τpG]ծ|_~~`nCؗ_ d8oŒg|٘MhW H)LG'&R)ep4T?@W/ D,}B=z 0_BހWWjV}mBkF}0銂8|󒦮_(ׅ̙+ <>4UXCV|}f}:7&|}AƗ=4xp)m}> ɤݣmrޮ Y-h9.NbkTٓ,-W Kff꫐Rʦ"fG @ѽT_dƗj*ovwBlN;OD gD:pT 96̾/Iec(η=560.pp9㠆ȉ`~yڸ[ʹf%FAwt'h2\jT+L ɉ}tA3l EBhJB`Rb }w>rM3FJwmaMii&Rk%УˆtYs婤<p#n\;{/Mi.M{Շ 7-F sƒCr7T:W]Au4*,:]ӫ%2?ʉ҆RHP*k3݅;nr,4dxO$DKD4cA. +tw7w9BX?9!#9L4Ώw7 endstream endobj 577 0 obj << /Type /ObjStm /N 100 /First 885 /Length 1606 /Filter /FlateDecode >> stream xZMF W̱a<$` ض@ Im9Q bwʗ%+r$"#qHHAK ⏪f[s`Z9jAb+W81JjN@I2 BxAh 3@ & BvojU؅`Pw5P#\P84LC0DA --K%pn2&07ي xP@( ֹ4p+RD.d p!N ”`)H6x?QX%EF~BR xTA j3ɩ*tZ KȢ 9;`.kgB&x  ;K*ЄlE jwnŕ!09 dBF.irX;CNV Ze?i)&YC$DCnϟk ̘!vVmN lYi2iU :0:\< ҫ0@Py2L.% bmoEX_>83_$O)]ܻZ?޽| WaeX?ۼ Wx^Yf{{yNBGAT9*Zޫnwx-i?o_^o^_m/?^Qw>D0ZgN3RSP߁4= ^|F;4{&q(&zNO6G'[gBo4f-ek,ePLTbyHxK L1[ (h-/Y QMC [IGӪ3i~R*2')E%qU )KrҸ3zJ.fz8xzx=|f`ςuMi>r:5pJU7 g]e]_:hfgd&jd}_0T˖bO FyD81&v$@u+ 'mm6TLy\DTOtE$9Eߕa}ݭ:a"yE4Pcg/_5y.X5`pQhyEOl>~6;X={q9d\?SwP69:f?[#NL[iҦ)3k5ތH&0]Xk+ů-edW:X&T^ {Ln1r*Gt94slNHmٔ ~Ko{TmJ t`_𪺯FI"5T"ل2vVLLJ aGHsL;|ZdiKNm&cE?|Pfg.huM'}dVZ,qϩa[rXjC9֙;<m˭2)7EO̧ Rr h~_F$]b*6zpT*'$c^JQs4v endstream endobj 679 0 obj << /Length 2642 /Filter /FlateDecode >> stream xk۸ u63|H\ڻm?%,Ѷ]?C56VΛ3$? ߋI,.6^Ƚ0wyߪWe\ͫ ?B%'!a_.>pqUO8_G#ڌ{~% 糟ΨCj `/xH=4&~ xTjG-ۃ),LnPcm3bj"}I+8J$(Dbԏ zl`%ݳRH5yHdhN+zJ^LVDBlH_2>!: ot-~ϫc{H9vs;XmB=5 'S_6Sy2:nwG*ϺՀus]>z˴~UwiX&M? R^I:]ըrL"{Ta ÍPQUsSv(v[&l˼9)妪 i}!f}U+'<'ez3:HӭII][>kTVoؘގJ3"hk ]:B6u@q0ș8b¤F㄁` #T2KavID$쒏Me4SUfyŮvNb;:>\j8gx7DO7ʪuQιUncwMr.I@WX=S~CUɷڸpۣ"ȧ*VVx;d[' HQU,pFDQS jI*7a(g JPWjMj 3K$'C[V]:/}I-N*Ѷ)Ga$$Babw 43lLnLuP֍N LHF泂 !B@B|I=0pe@RJWtkv v. GYs8Py J88zRXQۮo Jg(-6R|҆8THFO0f'pFG0KOI1;A4QǬ: )&<ФGڱ#ȧ;~ i3(lwbqDBȯZOál511{Ŭϩ98 'LmrUu| Ȑ?^јp6Gխk\}cuFHg_MIWf*{&^Jp8cF"|Eԧ Y^; 䓶 %P^' NWD?y)xVx ecq׶QɍQhͬ8~0KXI|"/IPA,zГ 3>Yr@E"8س}l[dɅmNajc)t {-y3Oɋ ;X -ZF{}RkcuXғoΊzラxP{|qRwX>hy3j!NpL C aM{Eu3ȏ⻒z3=__߬xB@fa(Y 6xc_5&+@f3qv5صY.Qyn&thE& Z%Zϙ6,V=,CH/Q"N*qV %Hq]Ԇn*;vT`Qk!~oJWIW1x:@AEIjчÄzs Y65Vp-KIh) ]J$-W̰juSsp?ϗmo!(䍥g]WWq*G2} 1'=}s˜5N@Ha% O%YCQ5M&q;fmQ endstream endobj 685 0 obj << /Length 2270 /Filter /FlateDecode >> stream x[YsF~ׯ sؒ7w][[XIXB_9paRY?}O==;z}r⭐^Lə' \ /#"",O_RLzԴɮjpkxyb cg!gh`IAU\")#'J7 YdeD)):kPI1T}J ,RJSDKL HsK0螈@ݏ7 C\dnlo4MJeTQ]5~W8ce73iw.As`QtjgPd:/ jg,U6 &ݘ/du9QLBRe$yk^(z#)ˀM92* 屓zE\ZXaSge[0E(F/Ym6f }!h, g^&S;ɶIpbaӇ 5``iJ+vIZྠeƱ1qӌi|m|y><e9 Je9/Pc*bUVn5:pJR29Gj[b_>WsB dm8 W "p4(˪]rQ6.$dceY%hLoS$ UʬCpfx$IJI1T}3|.̳zS<[¸FչӇw)$ Xqw_Z#0 m$PpvkQohbP  !OJp1 z>t c=\AǁJ*d$rSFFh!j[zdCFz׌|8$$a{.`rK $}y7s4FݚA9Gp}xP1Lhy0ԎQ 2fS"AbH Yc8mo8bBjVdw%e7ۿ,_/~VC<^J)†)L$f;)6.5īH9係 di*Aڰ-1gƦ^R?;B6?燓l:0#Z{}h@mZI{b"AAoPOS46Y﹃W[U ch_)U85;|C!_ D[C1|&/,p_3ֱl=J v`#FxWCBguV1A'E‰ө8e 8xbN_6*DNbEsl+rEen5[C wvr_`r-t%ȀD\ up+AuZTѲd1s_f-ФØaOO+DyaPBMAykqin j7H5"n`!u-v؎=-р u0gJa6lëqlU8 &}V5/'ʴZ@}9x֏&߿6׃%eP}S/H>gsoέ3WEtİ+o[: M u+Y,~;/bÜ%Bbw[|E>\<4p"gc'j78 8ƩTGA=C9)D }jYVd)~.Q,T:!OR1^[J< LBJ@H$BjF׎ZH@x_7me E1/u_$mvcUgy<^dnٶzyK#eEuo*@/81`}s'tE[U+O;>3q>Ycyb3"Dn 2~n o{*I@J_> stream xZ[6~ϯPK2Z3AOa6qqK)G&lNyHdɲsZ &/'~8TL[3GsnfZ鬜yr:{jTG}Lg_O^]O>M(B ,Cūɻ͡5"\Z!!)f>y3!%%ǡOA+`I} O"I$`I2+ɺ*\K0->ȷbʤQЫW@XP>!f b q N@c25{C185NIwSn[Ci8o-NNe 8:TMwot4 m cm:-1,iyz@`dKoi>%> +.Y\EǞjIIr};-ԚHKJbȢ7C‡O4zI0Dr/joR7T'Z;jMQAIAg0X3xV XtXM:/tY|n@i|"jG3+G~ѷĘ &t'%x,z j,# .s~ =vmB.˸纨"$Ax@Zɇ;M"M&p0"!$>⿪";LYLQ(*.|F×'h%܇\vųimNTZ|L Z9BNa ں,ڧɚ*ٍAq #,0'f2& G*{upX&} `[MSsuIpi9)O/7EӦ"JZx`Ee2],rN\6to֩av0ɼdGA:Avz <C$o:'Vgvu9pV?V>VgvgnK:#nl6M>uVۄ 6LulqVNQ6Oa5zwfƌvo sbo6hFIH23.veƳ.<n-D̂q*2 =lg*]Ed3ś endstream endobj 697 0 obj << /Length 2043 /Filter /FlateDecode >> stream xZnF}W `6@$ vx&AhD:$I~H,˱=ATRuN8p⫫2e(TFW Q",ZFwEBUMYDy_5+Y˼+m"⧫w\]vA`+HQX#\ GKaIJ4hFm".Ju؉'"HLaE攂3׏C!<@đN$!V!.38‡4陌T .Ia4J'j Yjt Q(iK{mɹ3HaC")4fv D!tT :v!Iϥl>H8qBф #BsZe]6ymDix\,$c iMω)MЕ};߸|Se r^B3QЃa>a8YLPQ^4B=xX=Yh"&y'hhXtƸh%O''zF%ҰOR\@dsKM*PS903Hor]jEK 8xH SS,=gHpUsړI;fO5,ivUEm ؚ""n!PbKuG+ *G9|t5RәK+pUwV+fەHb*o_uzגλ,0AR͊pE_JJ_zdI~sA+ʷ1M'Z]f,=@ާ(EͩF+ѻ-x[ɾ]˪-(E9}W mYTµͩ-oQpw̪6UUͫC`2_:]zZXm93_o! q<C~e^~=.)oz7xcza*E "<$6𓲋ɋ~ܢ/^7P|"H)2z"X)gS"?r_ `[ mS+D&+]+JW?WcV(]ƔG)qoHJ'Я fk#.0yeI(fwv[XRkD;S3@vx_P!-䖳~爱@}iGb%El4>< i7? ]iREIymt g6m`=?w\gLEmO_ endstream endobj 705 0 obj << /Length 1674 /Filter /FlateDecode >> stream xYo6_>@͑(ŶYbH4"e"K%?dKl,Hw3us9x{3W; {{!!un&ÑC]/g2+#FegMLeTHGdf׀*!FN<| w0"tVzA=u> >׵eAM[q ΠF[1B5 $F=xF\C^ ȗe P BF*IS6=2'UL&2k!iQsF#Ah>DNvB̷P=#Xͣx0sK@F`Czު2<.4W:̛YM'{Z3o$B\7EeW$2qӈRP9U@%1PtX8MnU9Ezc(NCQEy΂!YMmD toc3[箰دVoiZLBX)z n:C_q7ni=ZQl3l 20lhbhӪ%7U,9b,Z 0oA5(`\WܲʹsbHv$}H&{"$cqFѤ!GbS`y{!c9Yr{b۽7MȖ|I0Ηe>0c}xٰ[,I%,Y8QbӖ*-|W4%㥆a=8Oz9ޢe082ứ(a4s@'i*7,U9(l$=4$\]׋__V5;v܈wťwZ$+M~OF~ޠ^tw(PPI#AׅLpEaë7Sp*a ThZMOW|R]0q"p܄JƹT{ɘ VEoB<]EG͢T9[Q.cTt{p(6ZxP}BQC]L)Bo!@dĵ;X9&b;n mzŪozFj}`- w>:APk^yFT#ϵ_[˶lzVՂ9z-|UP$e&8foge‘Z~xV4kF!CkHxAݖz*-~zt.,^`FiR2B#a),>!jCZl~usdg?/wW+ |CزHr(ⵁ~yCH{C7@$^2͋"xjk3 endstream endobj 712 0 obj << /Length 2971 /Filter /FlateDecode >> stream x]sݿ'yBAdL3fr>%JdQ A?qk}JFx]?_tA-'Y*dWBG"/? ];MJyHKH"︄PaZĆ\286ƴʢYf+5v|=m[W{x{Eu-~ZnVե/Um?un+Dng$jD@9,\eo2d dt"Il`&hn/\4 İ0.$?]pջP7f$H*QD(| 7AIԠ} F"RL( yоaD{H&$*J!xh cvAh?[CG6uA+?@DV_HCU2Sa¾aΤ1QRXzj`?Cܗ임răh ȻK„0R!\;[y>ޡό$<;|G :k~O3$&eqwq3?Hbdރ{="WW+/K NM]־|:tl xHۛu}ٔ6!S'YKZLߘ`Q\|zPŜD{6~6tyw0r8R8DfJJ:} A8:ϵz+gtꘄ*>Fٳ4"5oLX*H+6}3$烠b# 5e@tUZnlq1U.+I RIL<"<|kvh]/-"Uӱqy9)gk3 *|pAtʩ`\ODܽ=-rH*{(ώF7u]3WōֹJc+t.wqäwBCrQV "(TFws`E:@ˍ4rE!KATYZ'i<Kj00W)&Od>r 1:c{o5R#5m]PC1tpIjoZB `}Y6$RԳ:fϪv"{3ʩױ3x:W_-9ѥ3:6d搮z̎FЃ7cƢuYcivjk7fưlUaQ1L)Wy2uDFWM*yjm-65 sbt@]'Eb.^tv@c@Nc@PcqH}j)g`As{4[oЏT5mܫQpKV_^M 0auŸ)HH!\!vPqwSoصhLݛ5~@a(>Vr4LAxҡCJח Xy=:F R?&(z;/ p2LuoP{g672^If_+(-5`:-G2UX`uGAS."Ji]%䉁X7:K0RҺG`>}pB,fwHPoiR$}Kk=ګZ6G)Je)'0t^k'tn6EMIx:]B? kL*=Nè?a6%<ՒLe}w) DZ46Z~DRo^xj;#C^Qt wMg*!+=:Ov'y';i;iwҞN x`JSȈ{?xS^IfP˦X揦pW$f]{eXz5{ }s<h8&< }]Ժ ?N^dyY > 6> stream x]۸}R$4@^zl ܵz&_lP/I3PϷW/s=]ܮOry\oUV-WrUIݘ*UQ%>a/^~`0UN:kXPxT>^Z_š?jE{)  `\N PRw.fJZjmfK1"UiVKW+k2nmWSQ*؈ہV.S.I3dT]TD0O4 68nr8('cܨﻼ+$l47RUfP Öo6CQoU 3n[YrI;W_B z(8z)Upy=CfA`q5 'nGj"*C4A1A׆SHf@T1'"MV2h1fo\L|5SsB% ɜ¬-? %j>޵J_\x %piqO+fIfEmBBᒢ LtrA1P 94|}">A$W`!PEn~S+ 9Lhp`O:*< ;X|Nܮ (-MCv>nvR}Mf02pf. !Gg8 ZE EϖӰeK%60 (^Y~w!#s1;/Զ̦U38ֻ_&-1?,<\y"\K_ .R endstream endobj 726 0 obj << /Length 2507 /Filter /FlateDecode >> stream x]s6ݿ4c! 2L{ͥv2DٜHE. -ɒlNN~], 9q|_" b+,R.E㈈Hia_G\Gzp$IŅmiJmM۳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)7"Z1vZqE4h a9{3g]5R)wKvq Fmnė܅.B'݅>ބ Vd"tWrElW/mxr)ٷ&bZRro1X`!{dP61z6:ive:n$iO trOѝ~((2O?9'i͏=$}m#(mUX|GNi0_sh8\|'l_Qli4zZ1j<<]2_QB|0 X$h=TϠ-*8CEKQAKWGϷ{Aޏw];GBԜZ3p{O6x»8E{cF+U6_uґ{sClFtz^}> stream x[ݏ8S4dI(z v{v] ęit:,Ip}[DIܻˋgʋYwB HzK0Z.3=I,R4O*B&^xuyB*DU1bsO-Ǚ#ƌxJ /wA[L@\C <Qt&ᛄf^Q3J P {>I)'Um)|N}=|o4B*+*aGת!m=|2΁1ỡ@U b&K]T4 9 %HP(? jo)4U$_EY)5mVX&ub9M Xz tgw~ >6/ӕ٩}^?v!%mKUeHj %y^NL%rő3  ͂؍y:BEu2.P7إC:B0E'U40K_1 }>f 6b&Z=l :ϵM:E+ϋ(J;inGU]x'ǧFq 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>F00I-l\')F(,˪JH!yQS endstream endobj 740 0 obj << /Length 2204 /Filter /FlateDecode >> stream xZr8+X,o),2ݓxVM2k(-Rvx)ٲeפ S K`ه˳wbc{,p -mIlicU\I!1z#] iym5-O%9k#+8`A͗%˘DRHPTK[]u)<飆4}vPACvG 7 B4j8o#b"dv-ǻ'@X#N1;bJDu!p4AXx`CѸFE$|DWka>"~j}r[ 2x_*SVӓnK!J#.;;s@{@( >k5 t!#2Z3O@pqbV:O aCxSHo4~0W9 5$%¶+elgF7pPVПv C2bXeVuI /znF}n0ɮ[ 75czJm(X9*К]Nm0+c`Y #y5tGB8~ݛ"QI;W3D+p0U<\ha-[?@J'޶SMP&Ph' dOsǩyJ^yi<ڕIo{o6G}9T a 8\Rn~L] endstream endobj 753 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]smPڷ=NQIr;4uT`4C65 Cƅ ol޾HbH"≧پ1x{7>uf{ORxEa+`)Y dR*APQzRa<0=i *;PGf&}B_1?ZJZ>^9-ATlPXAh'Xk 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]gvsjI endstream endobj 760 0 obj << /Length 2931 /Filter /FlateDecode >> stream x[o8BW6P3K^{aq~j@DWYZr7CRò7wYuf>Q?=# C^ޥ򄤄 ߏ|;U?eL""%И2S3ж>^#|P+78sϲr|SK$VKt_fWS9k̋¼ι}YW]<zEނ*q^^ lϛlIIBZc E]_":N̟}nf[]E^wxNt\FVtHGӈ%KZrH]m&_NA $re-8͸-y[g)9(|#>JlJ5;\40Fm2^M8TU\uf:u[݉QIZ)XV[= 7nMLMhh@aV.Zl $S[)#y‹̔M@KqYźf$ֶUUf8jQdŷ:-e^dS'"l!L35>m`kv҅W1c~ kXh*ip7X݃&9gƚN.h{$2C͋ϊv'F+]L 5ޞaLWCo 9Fͷ"IyW "DH "_ԍp7FuR (`"/VȔ`:~$*#z]ttE;]~7^jyå IN|qZ(4X*%0 -qk F`XZQπ,f {N_.ȄmZQf/!{܄4]'f &Q e s G6a`''=ҟ168G$uw;g vdMN3Bȳo:sB fug:7R* ǯ=4a-ѝ}gaNA{kT27ٺ֕<j up\ȡ/P) ѽS8i`'J7[TeqF/!FP!%4=(ahwuZkЙ04[jp !%[d_.Jc+V_҇5x E[Xt,n>Ȼ)&;ϾLcp=VcC+IB:831lNa@!(w^6٠ٶLB({9"B'C|, DUUBTՇjOïÃp5j>WN_.(qF: ם{@<';X7/Cx$3C>Q02ڀͺ7Hug?u}$1c1ܘ5J}GLW9W"jw&""<;`'a\,mk,34M 1ۂ3[90׭-9/uBP%!f1ֆb{%okۺKts [7\^ǴSI^cce0 {{]0Cw.]g'woUlpVČ،~ivTb\۔x1ogqH J{G% "75\1gQ\qxeuTGٲa}@0z-њ)op[ Ah+/ic=ݾgc_QZ#wE'7w/w3]8Y:|*^d8{)V~v/ {K;>y=5C_XOwdW٩We6Xo kWuv.:.z'@8A:h9jwx?YaH/h]7.:_.:u4x@\4Ԃ=ɋ_z3 f?*t=x `b3!`A}Q<7@]EϾCZ28>P hw3_>TΎ6|ʮ<…"mk`sQ9SA]J s6>$~}7|u R^`IA1]G]]M`1 E&, EN9'S0g%c !i  q6QFv̂j\TLFu0߅2]~?b-  ÒRQPR~Vlhz෉.dQV*6ji5 _s t۳?MGI_#]r|钩('T t/ca=[9 xg5&X.1 >b(r R+!g -k\4E64[n|ަ2):{ӻ*uFZkVmF9e&\MtaN 1Fˏ% oIʬksJ']Xbn+[ 6d7"m&w<_Jяw(HyԦyh<T endstream endobj 769 0 obj << /Length 2958 /Filter /FlateDecode >> stream xr8_A]V$dTdgS$ڇdjƸ͆OCߣ+BM_g!s׹5ޝ|ua(W7AL8MPuyᷟhLeQwd]jcQY[bD_}83U#8ȗg50 $A1*tPbZBKx3 %X`Q" 9kF̆IؙDģʳcx2/Ե+ Z zBwkfȩ[uʯS]#y34j@LFI A^MgaN @N8JBћ4cMzQ^m& Pj 5*MVY.~}̐D'ē-eb͘1x  M3~CXvUqMXץmf ˵\ؽx}5zb>Ts xO;Ө7$fKҧ,D~"˪fz$Ӎ;Xp:.{oвϖdE zeGzbGP4m/F7Bv"N #W~޶֣Fۙ7R!/8V9#jo5blv:Iǁ J%^2Ԍا]050N#:k z(Ssu ނ8MQ4M7>}8dvśmape3DJ"t #;ި 2 yv1GhEh=#_0܂ ٳH%ݽ^Ͷ4$#H[sf(bZ[ +tIZaQo=v@] eKͅFӭ[0s XX>$lƠKrt1 n̨-Z쀬Y_jcSjKĔZvH-)ܯ,&c4$$+[L젘dkI.&ٵHl:.]`@1%!v fb0ni(["?: 3H#}r`UlSA m$p}v‡GLPg2 z;3Ҿr[*WMw- "s2ns}o>jbx^1pwCcz@p :'rg\D e14C'͙w٪P7AaUK 鏪Fݵ4F1PZ6.5X|ɑ"bpMٸ*$;XVI t;Bz AygHt[b¾Oh &7] 2;t2D8gۓ{>,A ۾7yYgk¬; wcw(!w=12h' G>XwŎ~=ú-ϵ Ĩ"gxPq"@ t=Mv6=u=OMʋbrtS] >mc tFر苰0?5D%'a뻶 0J!"~ѧ1Nߑ$8('lVِzU)DijƂL\R* ;)o԰$ʫ~QSϤ֣T8z_ojq婋;Έ. xj*"ԻmKY d CB1^0%CNbekg3#5 )7oƣ2ʘ AUY Аqe9ՙmNu+4B(Ȏ/=pH;K̛?.G nGzEhL$زmv7ַ8p,w;>> endstream endobj 778 0 obj << /Length 1338 /Filter /FlateDecode >> stream xڵXK6 W7y&f+ffOm➲,kMdɕ8J^ww="i=z8^’Pr@1(/Jbb-wb*;LyU#'S1!Y~v3{‹$H,H"/;|g*y:LF! wA80)#=Mc{148jXxBDj Zj[ #^%\~? BLVBZfЪNvV,`(٢ 7( #.98hC"4{\ƛJGr7qjݢ8?4fM~ M6/rMyo4uzUaH$`㨠O2 Ӣpt6hnW5ne&~؛Ue 鎨cDmL)FzjZLY)sA! zu|'pQ!.Av^y̋f8PfHۦ6Q`֔h`ۏ%Z5BAn83"(5M[03 Z"}N(u2 쉙iR,|o]p]r6%<Liq 5Lo {/tb{[WإdsosRL\ lM"E lA*ެ'`UW# GAp KE#qtdաumNNԲ]u l `UUም!qMƞ _m&g.4^m@ݸpqE_g@v\Rp \ōbcqjF/֚&iQ+By@' HpyJ:

Z{ndeQ2a<47v [߶p_~ۧttiބс5܎$o NZSs>!Cd^\vPbxL5n;r9+_ . d4IVVyA B~}KČx-4/26r W{S[M0B`JvJOʏLXgVԆ\D֓ k _||]MJR9R0!qݝP}>79UҺc,击ih?F?1L$W|!M]C endstream endobj 786 0 obj << /Length 218 /Filter /FlateDecode >> stream xڕ=O0wm{-J3U|HI$qc: 0X{uHx JF9H | t-oVkѐz 8n~U}\BJR։1UHPՠ$ނD|.S+-?)5H_vB:%m5jޢ^=w^)ƙ78MIk>G-ߦO endstream endobj 682 0 obj << /Type /ObjStm /N 100 /First 887 /Length 1947 /Filter /FlateDecode >> stream xZKϯ1ɁzU%tHc'X竞]o6fe.<Ԭ&xR(T4Ksl*ݒxzrUsIn딨c UW j k5)H)Ԟ| cЃ!$me! פX:c=a$q!%&7cWph@~ߍ*5>Z 2 & 1 4QIܡ:w)БaKWFD*4ŜD c cĕa3W^c`QZ8c !`j`V@z j|5̈́1xC5XK;~Ns Dome0v-!V `/ӆLd UVco QC=U2xIf+ ۇ'X`&`(8WJp VkᏵPzz ᐡ > `)yl7A Mlb땕50 y-:&2xSB%mX"*$1^ZᅴoVﷷϱ#!WﶿB5\){;uGoƛ!a7# ݗ>gn{nX|Xe74LB醝sbj.p !dr/ oӫWi.}M2?&HqAH0*Z#-)ga?E mZܖoˇ}tTJBH&70 sgo"0H#8!"I}7a8iaX#N<Ȱn~7?~u.'jag0_\Jgvy/E YG OUT`ZyjB= l+^ZVUhZ& SRfzj,WzҌiz`BG?'ܧ>#FoM$(wIR_TM>?r='`h endstream endobj 867 0 obj << /Length 1527 /Filter /FlateDecode >> stream xZ[s8~1Ymmft2CrIDj0bU;i Ӹ^+L H& [p7mqwx߻Ϸ7 _֪51ylmC^sF lXKXuܲ8ޓnMȋÁm/Շ#`Fup|R&Јc]CE[ⅼ]48,R6"GJ9@mǽNAW n:*Q2NPQTe̵ L(l*<1ZnЈQ(<Qp!XD9s /#*m+R'h,EtUQGƠغO-9 䚞FPܰ-H[ߩrFNQyxNll`"͝ѲbWbڵy{ zd=҄"ڨw5;gyZy׶8H"li'ǽat 3hѪ^<zFG>w3 u /:R ΥSQ4V^>KLeHgZ Tqd3I-]~Y֭?l}쵤 ,Mڈ1u.d61|!oϲ+ޮo8lżCRyǵ6ui/l{N_@֧ݩъvz}Οaѝ mӏ[o7c&ܠ +~UӃlLdI_1z̗aާ̢*=ܰ#J"*r8yl#'ka,x- 0ƻxRq Dۤ6NN>G@r' gVs¯@'6QjA\Ug+o&2Ru)VyR1ϻQo_Mvrl/INRl{QVKnwGK'' s`W` p=ϝL{ H": NIî^LIנph }i4L]}] M$Bݯ>')_(G{&2BB "mnVjg?;- endstream endobj 916 0 obj << /Length 1069 /Filter /FlateDecode >> stream xՙYoF)Qc&A&06JdI*}F<\a!r3cmA++.V1VfH[hF׋˷/^^,6_sb)~g+9?s5V˕GahT(yZKD0}-~CBR̴## Jڢ2SE_ KKhxrq볂`FG F>xCK&ɂVֈ-eȒRl(SMXV|'WR{YZ *%-|;(9mΣ4Ae ̅biq̸-*_\+:^c n6o2uYy<_sM#5ڈ(MLz{6d]K9 0O&\LrTEz7TbeTyCNd!*B :ǼBYu; /~"cjj oXFCZYI%4&ei\4Q[+@裈T3t-R*qB)\RRٺ(a S'2`,8=NsT36rn>?酃Μ  2sJ|fkؼUO:z8ӔRi.^'m_=-ϔ5Z Jk߰]EEI(qpӲsLN' jFlU Z"F}A*QwP2S=χͩWi4`3_"(Z:M ԣަu S*# \XWn~## :- endstream endobj 788 0 obj << /Type /ObjStm /N 100 /First 918 /Length 2741 /Filter /FlateDecode >> stream xڽ[M ϯcr)"  J$!DACk2Hճ~zzdqͭbJ{/CiTl2aB.W?j{clB&qθB#QaNSX`+V]@+k/RU+BvpEfEqQ#C\.=Z7?9PҔf/7Qq-m9im(^RTjӊ6CYՓ0V%n([gu.|_=܂iqN!T<25u̩Zwix'oExaiçf/Ã;A|UW|p=쨘[`= Z\aCs+cʩɼW\0H\1A5d F27t'ŕYC{|% o,2eZifL+,xCb n'Cb[̆gû$^X7DŊ  +X^(p#c~~)篿zYί~XOŻ?_?W ^ί~z÷O?]/Mt±,->i>%?߬$ E4#(95Ǟ9^E#(95Ǟ9=J{(QڣGi=J{8qi=N{8qړ'iOҞ=I{$Iړ'i:.ckiZkiOӞ=M{4iӴiOӞz{g7?߿>LoϿ?Z?Ţޖ $NpouU}Sο{~\#VBF Yqexk]|O(&!ciXtwA6ږ>A Z*ܯfݸ{/6bXS Om(O4Yަ&(cAl DGHNXzB0}a_C6<q " } Q }@jX =Oj>GLG(͆"(^-#Zu,d3 ahm1 df@ B. ӣeìD= vGۋTBԾFꋲ! KCF[*Q "Q#CbhC l#Sigah3jܩejm]C9 ؝v<1Sޚ.E):mZXr*t&h`f jX燉a "[/l  AGZwV'tW)ͺ:MW̌mkl !pjߙ )\0!2Ԉ`[A%#4ژiPz۞A?04)m!mCT񮘚u O$qQ_^5rJ ({u~#"Ǝl"yvϭF&AF5AJ"[aH"b^Bu -Ho6[f9+(L,O8ņE;b|:(6EAB ;t0b@ō2. ĦV0\9#cܫSai;08dd^M1"14H(1a3{Y"D$6b̹P_Fck(@zcuSf̻Dt(".- ] ;<3Z"jm:!ik1dtL,!R#ZF1CVfmSA@M6D*X u JӠj!M֘LjkظtY3cS %2ndhB&1[C4XD5ft=D_RnGN''#Cl)+G6 s"Pp(Ba.#.lF4A4@zDH;(xTN׶[Wmš}K'M"דBĈ8(fJ8(@D !chrEoVeZ9ؾzd`Юl7 *%.ohOVҘ[E!֝ |A"X09y['[mG?^ҧWE+.|UDKfȨmG~p\?T ?Ҏit,ﭛ>^Zuϲ%ϲ%ϲ>ޗ-~-~-~-~-~"MGwQ9W9wALm P.m]f'[gY=k"u |@H0u2|8?ژg,@ e?ډ 0:8ė ɱ bh['x4PeF^*2A#N\)#CCo endstream endobj 937 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 939 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 943 0 obj << /Length1 2163 /Length2 12735 /Length3 0 /Length 14060 /Filter /FlateDecode >> stream xͷeT\a6wwwww&Ѝw Npkp̜3g~g^{wmjr5Mfq+Pvefgahdr@g Jd `acBt `)sWjjƏL ίR+@jdЙ% ..b ҿHBA6l218t]^ݺ1*P0|py ,,+@[s{k mMi M&= @t;]K[sgsKW -[ceyqgTZڲL)q-iP Pze[+KkkIC _ѼZ;C:ٺ: zxxظ@mX8вq~xw lZ@Sf5A*2ҚZ̯bpgqtEZ,Yd zyu1\ ZbvaQ?q0˨h1+KJhJ qW7?-q.!+)Aמ3[s5wusP>@+tsv;*5#{?sl[sA..g_Y_+RJ^n6:`+I+n?-!z-+ً`[V rsd܀RTze!gtNmiyf~M#`mnY_}\_; ?N!@XZ[C`"/?;V]iVkZ#@\_{;?P˸۫_j@^8 ?VCry@?p&9_u\Dl!{{` Z?ZRjr[?=- X6m0wv6Bf{m2nnX=v! q}U81NjayMX7`%:AisX]C݁q'AϦ;4l?/3=Pdz5# OC׎gPkXӇ `zvvNӵ6;m/"@Ky`禰27,'_D'; r)Z3h !Jr)z![?VNR1W'FaP^ ,?T/hKl{=z1y¨m5G,{3=[NNhח{^%0QxǞ?&n%17a;{ =ZӰ_g5{$ao΂mӉBSI wb8B:V9nMſ GnF$ә՝(&؟n&ZC-/5?*ݨ`I6ה' KH秕5Ռ=Ol{S'AGW`#{2ׇwjw-i3Nb$i8W/׎9:xLe{\`l`lo{qߕ[Нmn ZnIc;U3nlTmFQ(3> )Dip0r| e3$!3oуέX_!pNNzO7d} $qIw`z[TVR6Mtٝw|[icE(6&ԏCPe{rKC|[_-_ RK'CWp/ʴ>-lͼ3|#s+krkz0@k /֜,N-&A84+^BN!\\oXt6=vIYmhe :VKycZLPrkq9MaW5 Q3e{&]N!|JܙjkvSަk3JXw3x@.s34'<-Svk$З%#ug "{aad.OD>RF 9yqzz>_kIᑜGm=z3vT^ۥ"Vm9M&qd{" 0fO1sf{jg-G#Ҟ*~">)̩i}l0W2W恤3Cv{Ql{/} Kmw}s'nاՒN*s_U Ny0Oӆ>=WF0A یDed4H%!NRᲢ /ǭnէ7lYyEL6Q(,"-wCl+VYXi/?ޗ,,a܇tn Dn<5`3J[ (Cht C~rq۞c G:1*o  #܍x.vj3=ypҷ́žT ¡8Z @>j_ٴ -˳ɦ!40݉ m|ыjpr:j~Ȭy'g ĩ>^xpw^4OTG+,'hRFc_ rJY\,U\P~\T6_=~ם#5lSҏjF!_ҩpCAgWCz~?~RV_%#PCO&DCO}*OdpJ7Z (:b71ry53bhHY9V wVs I@9db{ߓ|f\0+! c /cxꪮ/GN*?6L2^b}/ ՈU e1ܚc-45>WE_ EŢ?XRFJFٮ]^`[) lVCX%̕uwHsku7ַ wܪzEɖtDnL%=sTJгD# ՈI~_ؕ^/L}u &‡C/3vA}ub~$fD͝.„U|RDЂ̮3#m9V Q7Ms"PFIG*V~Ѥ֪6(ɕꯎ!J*EZ'aayu<m]L%}ڏS~RK֣)mu F%_[,G|?߭iE& j5M4\DCDBg* \*R9Da-‹눌Dk{X{dT#Hr,Tۚ;@^!T.C=+4_͠m=R~O}%>\z`KU3 ?[PX9ot xzr ]uyʹ*K"xEHXk-3d'@^ЭA{OՙQh}LM&-ɾZ/^ b5"-`Ϣhq?I 6Gpd1V [Qk'Yq[8 xv,r|z '3`p}tVFoXtRԣBC>z)P HYaB(Ӻ|)W{HVX>JpQvC(CbWɻ$&齃D/1aЄz7usI=Gqr_8U \ЄjMˀU5o$g )}W(0SI2p=h(. :99g cRὔָ5os>#cW kRҀ֔:hb> F>1\Mo Pߌp0ۡ gg۩5 팆'}ʵ=|Ww s/ff&oaAA#L.Cǔ.4ӭK*K0n}]՘8 W#T~WBCmcjo2IWA$,*ivX]r VJo5%bS8ۆ/.}5ާ߻=4+ >`CbbKZ3D}aG۸Yu0DuÅ$K8jGUv3i쎘dv<;U :%\pJ2;qʛ=oك9XuQaj> [ǓN7S#B}IA@̜,V Q p6|/Kr+~l)4m/^E4i&F1f]b]VjQiEa50~` "F%S!qz%reE=)yLӳJ';gqK:=_0ɕXJһB sTcN<26,268^aFJ2Iw[Ύ)o d 2!ڼU9pϖC/Q#YC4ߊ6/TF^t"{aZt89WT#?ˆH`IETD({k4BȇiV7:g͇MtlmŞb 7 lda"t~ϒ3\^?u]'Py_T8p{wyFHMd9QfL Iל Q(.-=qV,X;_bCe|eYg:Qg%nuS5Ve" bKB/ oDӬ@oN,sJ]KrM.Jaؐ>vnCo^Ts C(L3f߈"lLKzot W@-`j^41>$}C7$?]LۚB0̒N–qLֺ8] {rQ0۵.;(IQ @FqM,daz K1q,e/Dٜ^:%C~ C?H_C&8.,wtbO.\ ;k$f/%H$*+ؘ['Z)~0˾/^Z$;(Yj t8 hJjLOU4|MG؊Ŵi+\WdiY.ױIƋZ3YDZ5J$g7/>Bh`H5(`%ȿI>}OYzI 'm9V_" <{ N_óp9 ߷g`4P*Sou z(' Uo#zH-m`aBϔbwe &3r~:ܓ8A91| t@j /i+zTd""E ԖP70A)=36|~|MPWq?K-Td'Og$M|OˆЋJÝVN_*&yߏ/'Wc[ZnC/,J9C߲[6Y8),\-=\܃XPy8~nmhJ ~ro16 m8GղtZK-)?8>#^-v5noJl4N84/mGW[>?1OlxLIKcA㋸T0+`(sFsǓ1_e{^46'uÐ%3 J?J$0@RndBWSy祝gfYfbP=p"w|<[q#܅SObs1 Sd9$$~H'e" W=]=̒l+'`\-"#]!m/dzI>¦A~͘ݻͽ4O+/8p䃙!WՑZKh- =+":g{ طq8Z1fϴ ˹B%(PIjo8@ &3)o= LEaHjߴ($oX0Ƹdw-!s;k~9K;$' =Q3$Yʺu!FE%U\)ad6-;tZm7ԭ=3')} {  FA<|Y t22sxB_Վ5~kbKA@4뙮%f5bBkb>i#,9[r;; 6& )J\}}2U8|y^#;7WEd;.G2~r0mGE;5L3 6zq),g0kMz5m$ H@WsZj&m+a&ǎFo]V֓H2zh1ȳO0`vvǷ*n0 E,`F J_?Hs8+ " Y"宷sۨ+%{PsB=/WGa )G )cv?džr!4 fx1nt\h6Wx4"T՞؇ h78*Ya~QT ƊIF;r8` 7iϑaO*oO:ᦻ:%|O>C$&4);>$[=|_h:Tf=s2hk1`XOh/f I I {;ov%:GvHocHv!#W4z]8.lx j؋0`$:EkTh8 -R$Sj)LSu),"?\ͳ]E%P`hqѳ'w+M[]`~$Xh{bjhoZe nXZYF:oqBLP'*f}vԏIDaq]Knʨ^';yۑI5eg0OsXO4( C{v,4nbۧjN䐢_5K8#Y{tUV4Fh6~e* 6+Mx4IXx$_ƏRA_}.h:^(w;L9L*`Ff-\i,)쾬_BӒ'H-~Xf#6d.Y۱\xio*'6-QZJtρFdN"aTm ~`G|)0vkr^y.۹l>).`19Fԥ23LOVU햑Vl׃]]C/z1W/`vt8J_QLRb-^4;h|a5_t=g,f ~ڠ`U)^7'mVofWd=mGK-e9b ocV<|C}g4pZ"y\Pq]2x|`q<\ޠf)}7<3=QQqҏqh!_wFblu24f(R_ E''["W ,9VQu0x>u6<;.T;LHܧJ@"q1\cZN}bt+1l&"\ NK?Y]\AId1*~>Lx_=YQa;$c0,ʛ1~rbp"+٫s둵22AG,F`<'\Y>@9>6B0MO@I0VMsKvO8ygUDe|znbYqaI DqcH@%R IPn'Tjo,톬_l#>M/(^5P8N8XZʈ>b<"8!q';Y_+~ev`@uQ 9c4V0C[0x; 6<{M WW.gцT&ةJ&Cm${ŚmQzKP^ a^ hO{{AVkuB5o4A-Wb0 ϮN7rR k m<nk:梜 tnk_&)btkR+W+|Am&B*nI^D JjŜǧ:7 ?M3:2s3t*M){3zPHz 5VY31,JDhwv+ܚ= 7#QHÎX={JM4'J_ɗSgB$UD:K,_ibmJ7^9 ACH,9ֽߙ!u{jUk^#i/:ua7P-@7u_:hL8g~P@5Jꛖ'^UPs#C[<Ħ&m>Ͷ hۗOoԂ˰0zxlQLVqUj[r4jX!p8"fΤ>$I&z8Z oN\w-PqDW `_Iꔛvg՗fjc۾jyEګ/L;Iɤ^FkYqYEv;LUHi%Jf iv5*A /hcw~ HLhԎZA YsZ4Dln!yqB έ@M΂?@؞NWI0_x{:/F:9 ɟisȪ\k8T ?4#Mn iNl gQH|sݹiK(~(*QmAIѠ W1GXIG _#ٵۻ3j^p0qx/Z"0\`@V|/|#_EF1h =-x`Z=exWE;A}`S^0vhy;K&UZGu5+iD' !~ ֈʼ; {8i+[?k%HK G:~XKj8Zh]_oі m!1=^%!|\\#Nf%@Al^A97ͳ_]D2ؗb\3"Snw=K.ʚuNs7-b*}pb U\_B]%$墛ᗷ.-iam M u|vR et#/_߸JE֙gJ] _01"/Ns y4o d' J=9TAXoWC9kiyE Och 5JbWQv̽4Hc݄|{y}bME)3ڧ--lG9Zw81&9,,et,xa',_ʢ}>AbGy9TbU5EV\}Ms^%gU>C`] z9-и3 q&*~M]hupAj;I۠ ͍6KuFmAP9;|s5T[^KsIB£oUQĨaPc?g/)?% (Ӱ{WN>DF E>xӚ|=FÍ燏3&9Τwx,7=)Ȅe~ϋz=ФoʬMIqBl5(յfZGEj]>~xhkYo# Bx6j.srNfᏈL "ssek8U݆"j9}zQFͱgq{tPCO>P ;s`F"5Q_v˗Ȳ!3pj ڧ 7mc(㦣 $'2*)#NxMf  AcF} (J0|7<p~+<"݂esNVӋ:6ۇ8U,D *xOLäGoB<~w)ޣl#%-7IuAo1#LQD@NJן\VT8YlPz|}x̦w"B~1'<**({& B&Xy_J!hV2ELe̋zK AiInv~:Of-qNoijr?${49bq=腚jqQUPmpsSfA;PDI. Scm+?h +0o G.8r֎ _#jDP=CWAVX;F蠀Ʃ$M}PbKcXݜ;XT3CI>%sj!+6 NV}*i;}j"s|`nzy {eڌMo׮ =Pi(To߅j[DbF H|<{37tGS/#HE[q#Hpo6.?9bHQ~qZXmi]!H}\YIg8}]7KS2 q zRHP]^8FXV)R6hH؍/^𬼵4%U-xQYR)tҋ?i/}ob9S(u6e%?7 (XΤat+0b(8oK$YU qբ_ImlB(~bXd\-.Y=:`]!O)1K(P1*qv~4(S\!wңG${G}pF5mI TmJZ-G*}zt"f/]pL*n6 }4.uu|05dC*/MW3./݁X*{Vk].^Q}s/pe28'YvzqɀKXQhl:Gc ^l}] 7c% rӠN( p*mQwNR(?" endstream endobj 945 0 obj << /Length1 2315 /Length2 17407 /Length3 0 /Length 18740 /Filter /FlateDecode >> stream xeP\ qwwww84܂{pw !%939z[]~eJ¦ cΙԒpJAN*@s#G 9#d'f 29[Mߏ01qÑ$v@w) t6R2%3;hgni~{8Z[8IG W@wgӻY?JE2F& 7'kK)@Ar{'Z@vcdW*@REQ]Ia t3:&FF&@G'ݎ2Y/#; wU]A$@LXAMԠHމ9z34#./&$'fM?[8]d͍ٞəh`oCǀ{ Gk#7.vq~䯂?5Y7( /S^XAZB\U=[N CQ?YV&[81t]yp}vbN⠗PTPWP$.d_ Y9uYNII`kdisFv&998H/)ɿR8: MYCۿ-{[:9;K#2mNz9G ~1ϰ(o'?-!f^ g}k;YڙI4ŞQ(-_$h@g>&?d?xكfF6N@K3{:}7"8fNܿڥ@=o }FLAv6}kǨr~oZF]*п59o4*,$,݁J&տF&lgn|_c>KM@o~7:9%|Oy^?>%4e4ic+=*ng23u{߇ v wϟivҿQ `qAFAL ?(b0ޭޭ)zzzzzVnOnO?ݺ_g j _Xd Դ4}j_G䍜-uG_?IܽY,1ٸ|ɿuaH4[Y|)ϟ,&f8ВZLl" 7Sx}튴ȃ>ټ5'VNܘ* m! dk0/PdiMĵGD:~G^%蕵B2pAs_@07FLQ"͌a^ }WýhoO,1Y}zќ|BV=V A}S]GгʮǔIIږq(t_SMV,hU gm'cUNB% +]iQ{Ӳf'!%J_(6Af #8TdF@W~ߍ Jqgy^FgOTЀAD).3|VjV #Fh[$[0if[vIBZbeD4\O0Kp>xɥ&|Di`#fy+'yrtF354Zž-z *@| 5 =%ŗzr6O!׌/ w&&ޅ{kkIT泓Dg:ki?WDF$nQ 7橣R}`+QJJj(eWD{VwNx oƧtN{܏M nDU'wA'JOӵNN+N!B@KZnȃG̛riԋ3N%: IMfE(HC[Ukۍ~ _IBM<"Cf"9+F6#aġ:CHhq@o2ʃl)Ir q@]ۆ +~sƷMD =lZ'vil݊l-RI0g=$}4eJ=J1/4>}Ix|n9CRqfQP'n$۳{^^ȪfXxWKGAx"v_9^  =b[#e76L>J,jVhzZD_ͷ-RMg?pgBnnIgӕ g֬\!N![y`1g'oyS xx.9! ڲNPG-/u Ci)YWL?2r[N0y<42LcF:lZC24_a rG7>'MuslW$1FV]~b$r<5˄%Hێ_jZ)YvUQX* jlci Bm@ 8 ->qsJŘ@J]BZ ȰNON;FZ@e`k'ұऒ0>XCZju$B#ɵTRup]Hj`aP9ni.̾BUkG6V^(Mr;I5 zCs) /4otLѽS}e%IMhXcb{˕?Ҽq سڄDZy)LLŚmʆXܰc@ |ҩ`M:aJYnQ):SwndlT\Xp[Ǔ2müjrJTivU*LiOc&IvdeM8'\HiU+?o#oI/`?b8tXA*ryh-ȁXٛ,/UMzMVk +0͐|(%DHتh*5}'l[Pa5y"ـ,Ln5Ek34i"vuDA>욲|#}V_,>j& ̎Do&tgy²x =|e{dzu[LxW].AU>>Lw V$Jp sfZVѸGm _–¢> \P] U3x=c[:u׀ I)JQ+QǤj9 ͩI褁hjcB;Ӡ]/-ilv@þ^a"Y?X+).V>kŀ% AYZc|dirJ̜Ax,Imc5> }sx%p^H4~dKd Qjݴ 8_ ~hNG$JJCeʅI4u S)=qlM:GItdB=,t!OZ#[v<<ŠDjr7ac F3[2u(y=u6ň+F.d6wi-R\S;ٰl}GۡO~i^O/+w1[ 0os_%È Qv Ɔ;g6p6^+03 6ݕ^O",Vo !M#ᢧ%إ}eO^И%M4W=P{O4zZM"[z(KW֊d:cLƹ`|*h^TATG ilh6u "K hxqLŢoEYՊnQZ4_sյj|372<]¢2 % jO0|Oi٤SXlM^,z e%AwP@ö4lW2"jk[;,GgTl"Vژ1<D4;vWeaxB ^=J^BBܘmm$-yFQ|Bm0J/m\ָQƀVdn c_% an)NN2xFKa[r}eHT jV4-O2&hj )V9y+(_4gDMT+0Rꬴ#CH9hgGM- q-w1v /Kdf}/dz Ϗ2gct7,""~Gl 0 }NՏ-r'M+blݚ_rEfGk=$ abR{4E 18{WFB3sCs(0_w>_)5ғYXP2 F 'JUpcDGkZFm_ d\1x>/<lmoN$OD900C)&$ŲNY5D#8Agc>r"syT!nsaB|©6J/\ #xА⠫IJι[-v.fh1BWw3uq,ٛ0d4ދ~X,}u+tk_kG] b#ODŧiO"uyRZx{ֻ2kH̩6Z,_I ܧ&lc>ZMqޡHNDC/>$i6C#[eGT~kzH=sAx&f A揃[eȱ ~*VIs"4A{"aQs9y׌FaPbEv}/i790k "d全 `IJZ }j߿ ό7RO;0#ȿWj|t16z&&͑ER~VVp=&9>pDjVM viU."Ap)qjG#ǚUـ=KSB?0a!}8Pc0~bþr*}K&"vYLS낀LKeϘp܎ Xcݼ8.%H"t ip2¿UۚYʀqE՞Z_Eʮx[LsA8O> ɬZۺډ:JևX"2a}>Xp0%hyQ6dҥ,<0Pő$K.nABebJ$hh{"yv-Cyh{ܠ{S :Zg(e6QLx͙ʹ7b[mgS+ lyM%V(/Fۅ`@_*VZKCWүOv# I/m H㯾D P| 6Ιa\d+ŵ?>^Y,EKV, = gTj=2Zm 7D#$5Smc+ͧoP3 *SWrSO\x#+aJkA8BWy4c<RϽI6oo΅GBUk İ)Xڔb' #\X:8„CkjFG >ԖZ|9Cϛy>k~kVhUlhxEIE3.!P*|2cHk4JӥDt, |/ƛhfPj1V7 6NRtK_n1rװ{Ґlb}߉Gρt{se1^IoQx)>qWUm)#轌}vY]ۊ`eLuϿ!z`k\:ݨBQW^ƀ*ўWA8#?`5衮܌彷MC(49p -c@f^o t6 ;$G4 ZeN0*^ڎU{n7*T8U~⎼$[ 0S~)r2-Uf.lmmL<򂓦^:m~E){oaX3ب*:AS>=9%ߠ.mtگY(r|&T qIgL!.LҔ #ܶ nړ﬎TrB+q Ι BCmBXMe?KWfYnQ<+lݳݱcqUK# dDuM1نmmvSN֥tIͤMv.Qc]w3E)p.~^M(& eZT*,^Q. ?cwfD.w]ـ+~MWƧeg9~hծ͟D\B^+"knįA0zlO煞ݫ mL( zz`í@Ofq n}a|Ax-p ڀB qt='^&d +x{^fNTV Ao[!=fסU$d۶ .2$G4TdvEq {( [?:PeܳEkS>R#d&tƺA:Es 7VS LV,\<ת}>w Jxc[&'aeXFqĭWgQT(ZU U+:ߌav]5]W?n* v 죛ڵZ->~M_|7֥ؑ(Eup";iM/ T$ywCpC`}۲7soKiK՛C$lE v(mG0Yh#L͙/]}O(&:gZ 4~](n uܥ"V7m)qUWZ7CaxcHS"ǐGN [S=XT7txvIunlDZ-~S{| O3ܿ4&qS4\)8E[$=k0Z? wf _eU5YUO ( _Ayha85*; {XIJL#p%Z9"$J~F;[LS0Z[J@Q:ńr B|%W^t]2~~SֈF-i-|?mE9LːcxXLj!Q%nKGuOLX@`Rz[Tf=oQFbwKG,5,銆Q.1;mB#b/g-'Age3C#N/RgMIjٵ?B5 NHn(՛n4b+. :,X2?0ϣ@_#k[P%%%U)@'.9Fs@Xonk=}#ALj1&$r*&?&UIAM5mTw| jފ j;ֲH z<|S%wr[ahbbAP;+&;bXe$e ?lew<΢̰l `.Y\ȄI Y3G2?.m81RQ5ǾelL%`o'yTQilҏɵt@$r;8Kz"fk2G}ؔ6*=(-`h5[DM> ИzE@aZՏfEyrKP-}g᤯f2z9rlM;T9}*~]8!UGUms77(9(z_5Sl=m7p<7M&wѷpvL'귞I09Y^!qAxKIm82lvR|mڀͫzvHǏ@櫉Z=Q>94RI8'}#V =9Սy޴Q 2ů҇iTHz=J΃ls+ HX13`xa)b89]Y>}~v!ҽQPFFla sGICwe8'Um8Qcj(ɏ.Lsn.[X(Z'LM %(u)xUurmj4ynxA|3i4'JW}0;PJ0a6.xES|xնoTfB TҭDJZKjw=#$f[/=ŐeRQyJא>q/aTv,;GϤkWPIMw6zB#I7gg~c o8 "&dt= i~ÒJ DkE`)cg L"0԰,8d4vZU*J",?_l]Jb;Wrئ)nۇkKB{$Tv i<ϗ Ո͟2e;Y9w 1V`8LKpXc`xë?46㫫KclݬMRܩ?hl#1Q,BE+d,[\/u6W5iu/sJqkIDZ>ЯQmĽg=;*`njTԋt)&@]M8O؉'ord̞vWt }0d޼'N鏷I&@\vPvX;(4nGZHEmF 9e8X7? {HAiؑ۔}<$I(zsބ{P񕍄CWMjE($P;gF!uOB:)|gTՓ\#p`abvHE#DmBC*vRB6Du^gJ"[5哔XN/# $L(.ȕ|3=s xMM* zu,*쎭(Yᄎ=CU)Uԝ.xIԯrCa{RM'1M!3xN]|Ki{psvR=[<>8tt{%,3ɘ<>d`TW-ş]r}.pzMG7Kd@Gw YG.>dK:`OOSlsĆ`E[YDF&]QFPR ѓC4ix9 mÙ G`~cիrV`׀ 6 I"rnc7&&WX(iC"o,¿́u"(K΋#9H;łtʣ:I:¦ۉ9 Qon5-_aILc$/C[gR>ݡM)b0,{xm)$e4-w4,FUSw̉(:.~}YWÙ'Dk8@، ~mOIRݬ!_OtR'<[ӳ`v_@!u²N=T)!q<\tT:jnޔ$_,8cbѹ dw_*\"~ +;ys4_zN B<LEW|~g6^C;}<8,BYq$;*үRHBfpBBY_/{(bbŵo|yu?O~NEK<5pٰ1~D }bL[LgJ?rVD;f}+LAnח^if qC.2A,"aE|!4Gid2^fPFAe#7 uCMW'k;W4 ɕrO$0$oTg ԃe(l~1g([:C9ݕThf T5e+>.]튋;blqAE2hG*G>0zK1*6jc>,fn L@Kk6ytJ*Ro7r".-D Ġ|f'jS2X[3Peu5±ƽ^?b"C-9!7xZ> œ%Jv- :9HDf P{&$li]Hbp,q&og*WfGK8[UH3L!#Hc%{y +^{?3@{ @hƩ?~ 鬒@M*v33k?-2U=nF6ySO;Cϓ~-d㵐w-t1Ddf<`[>E mҎF.P B&)+hQf:Ȭi(8L0p!y nnw'4ܶ#exAk'<[~%j;91mUV삖&Хnϗcʠ\}|*004/ro"zq“8Nb_Q(9>':^-֕$Wq(Cn9`QTk4Ez؁(_чA7W0/tCZSzػ V :PQ4l;KC zf{<[.j|ö){@7ZFfZ:kER\Й5 Hx*D6 .>tX bjCa2z .|HZO&Lc-h89* E0b鳜)b.~.dM ,uu`bCAyx<>2vv<ŀ aM b$iͣg57Z=̘Y7۰ G_B"B1~-khDpRg1"3J[rlMq#zmΒ.^Zt~@?1Q>9z<&(nB`Iit/`:dUJgV PY/S؀_h-E KpsٰOTOD#m3U6 l,2ǒ;OP?)~>>+ 2 B?VNN5 )^ {2y.it>C2ݔۇ|>G; jV_^MyS2solRLYY-!}NͧvQPODO_^m 86%*>=ygx7T rdkMtT]=O3K*g L B˫pޱP"ϥ tu4:|oϕBbk|f02(+_D%/ Ee%ޤ!| SWwW;+卑^{BꟇ\s+"U24>CJh} ] dA˵ceg6c0`oM z(o;ߔhHN[7$Y"9aNm%b8[AP]?q۳±*G?2eS & =DsՄAއuz_4B,5|61@fBQA{.$f `0j/-Ø]F^#vS}_cPN3Vј?bz@!(So|SA\iG3~zCЊE#ewt/'J{|±/ 3)k]v`,XFng1/XܷoYs~o VM\ rk ڗOhQ#=he1s 6յjlFGhqX$T[zU/?@[:`& <W9wXoi;MOB'"J)Sq4J8yyޚ!z4Y:rM\kY,ĺŤT]EBU 0٧j]zf2)hX;yszt!;݁,mgz('2od ΙmQzh!Śg&5°c\mYBV7r4l(x99~#b&#+A EFh1^xU' d]`qa-~K+k]ObGOx ?GB_z3|$sThAYEF)mCݒcZ+jyม($ \0oo&<;T+)C9ưRŮ@Vס*/;Qi k H;{5cxk+#Y )W^`m#~Fx'5~xREqk!9~_ /sӗƆOWaRr2{.ƔEђp?L(nf˗x:hќ%ZC.\ [.x47Yܿ'*A J 6e6hG)SաՈS&W`Ap4( vV%e玿y{*N$/Ys nN77nq-dGh&Zu!A# ίߌt_/F$B>2N.νn4_f4{Ґ(ał`.lN7E?s8rZ1deK,0CC84BkE\\x(ef7q5QgHBq`a zy<;FEzN8dٓ7t^=wd`b:=쌌.vg$>2.R>Q;٫oz'xz%Povђ~aa:r֊> o݁沓[&ӉI|s[md*O:3}y}:2Si5 5Z]XW;@׳mu9:f[_Q_kpFOc⫩}d6CmFVI_d-Cm)7 d ?m-ʥH;s+1r֍&%izuݬDoeHK4"%Y,[Uef@j7_Tk|g@*TYoi˕T2 OAF`Bi0ZӜ`m@>qy [5d vr3F?ҭ b]lZ,lk&au>{SVkSzA-|ysN0)VQM3GQ5e$ݐx) 8?-^wYaѢpnD>))! nfm0l&:Ce\6D y c`dzD+@YZ8GX=1_lE1qb3k\U?\F+|>-s #*FŹl`ȾoZ;5غBkFi[xrœz]s踷dN4 8!^Hx˲{(D67n X mub2y>"ȋl3 rbXpT cЁm';+5g6Kz9A/vʙ_ND1RmZ/++V^e]Nŏxܒy>%xpyvw`)uwշl&X.peQi4W@ҐK,8E,,sk%5RJp؝)*XiOƳ4Rvy ܝ!wRӹai Z?0ᑠ%^ort*N|%YiUrwFh(7z#i3ZQPܽxCn׌] 7@#Fqn5o-l : PZ>ڽfe~77(*h#pġV_AЅvzS`f*wGFFE$Sb" I)=Y2lS7؀#~׼ @jZ }LI$5 o60zi Tjq(wjh::#(音)s8 J endstream endobj 947 0 obj << /Length1 2840 /Length2 27003 /Length3 0 /Length 28591 /Filter /FlateDecode >> stream xuP߲ \ @pw \kapw ]knss[EM1{wﵺ{) 'W4EA L\ePhgYXXɅf @.)@l`abHZN#+@蠯jdPA`7H ٸڙ:N;d7hmwR! omdaȂF3`4շ4I$SSWf€CS};}C=G_ʦ@GڔLQd?UeE@U:2@Vl @Lw3jN=~3/9`1?jv 6P:8p12:;;38;0Ll,olj-n0@\K?2"Jj.8:E2"7A3K?d3K{f2X;fU忸@p[1=,g^TNV^ZBXDVI ? M~ ҙCYZ^^`of :}kC0?}G{?6hDŽvve!X_V_SiK [3;giYec?LS,bm$ G=rٹ2Glyͬ~W`hèbmf?&?6 eCS[s~t-큞f@/Dw{}'9=v'Bd:=FA'1_f0gͨW<ƈ P2/֢TJ}+3KZ-H`f/j47s04Wep2AkK T~_<~?.L5/\ noFiA)aMIcY(bm226O8;@N % V"NpS+ 03E\ Nff 3Y+o +o՟-a4 ydWY'FpM b0,`f| {9%&A0?YM]mL`Ap,:X@f?p0?`16`"6@;3_%e %/lf.60;S;_qpgD;N`o[HNANZ NDC݀v?G?<ӟ??`%;P};3-& l@Wŝ@<.>rzG<o ]K CoA^"%0˰%cR&[?gm|}R)@\_| 0-_֛'dފd2,PHfhN64TF?vG]&h4f827`Y,-NqxLj \ x3yoOʐ}{*7ěLF9^P";'O-'Tus rŕ7ƍ|? ycyd{I.izw3SGb O_3['xҀeQl17IDg 8NuFrWw8#rN9 /c'V 3T:g")l*ޓu o} ŽG1zV̐pW*n-U۞*Zi&iO{&&OnպDg}i-D"}kx?ğFO|VpNz8Z]cn [nŲVLE] >֔8ZBAR+d^e_^ԅ$,` Յn2C!P|TV0sf+FMpI>ڲ[C^A"`2_Kz"[U6w3$^4LA[ !DʬhdjTҠ 0Q*]jfΡIrA~HXF{!oNjlsKE[j^b[Ql2f)O7%h[(t̥~G Ө;*}j\2}J~MœPj$~Oɇn$r&UGB.THWXFz{[8zܜ*Al Ɍ 8)J|[VBp iYNm6AǵdI| ]+=_#>7Q!c;6c3f_Z?{qRўM"Gm\'1~1\t7LHEʤDzDVO{◜~BoWM!TWR>3o$? 6U*ZX|]fЂy#سVR(!%) b&W˾0s?ZEsW t$J% ]m}[mo* Tq$D$#;QY93Qٴ[G"\4xV%4Buuxa⳿̾ W3keIkx52,O?hϓ+LS!{-ڰi)v&őOE 0܇M% !L'Xcߡ'h6O Z[y> ߖNMMoZ:dpb ~.qmդZ[ Ev"iϳ ALM@0r^~'{gv<:x6[0xI(lstl˥|TEU$϶wmqx$IN)2͸qۮܥrXpGK1+}ȓ`6x4Pt\s~v>OI\]S5.ڃA)C~x FeN>'4W!A^Ъ+VU-:6Z:ۧrJn9VAI)YCƬAG! ~t잫AC S"bb'zg%iE6 >FCXe[K5̰4JfJɌؿ2:i*FxJw=?(Gr'~ 47i^ċ'2 !nu$W 9Z^m3['n")2$_,DI&;M7=G&ffR-zՎƻjTS;:MSw*aR{TU|ȴÎއLdEW( nf TG C邬{-d? W-fm_\D(ZR(2~w- ?{!վ~7)A0StN, :ܰnޖ 6?P5n8=R)Zp% Y,& )oħŇs[yT& z6Qgo4GWxU =Rf:JO :,K K2.=ڒHK,hE+ɗBSV[Gs q|`"2ݹo{ ԯk׋S2 -cI{-aXcwYOd.8@THdbPz(I)z 16Q(|ƌdڛX84 ސEf(ո{j~aa!5 ZA.ҧJRĆUrm,zS` ^cB[ u?7oA{#\8gsY=.`=~vxXP6"l'3c۸CgBg+$8l.b$,9 bzE"Y|[փvDb_w;|laPZ9>c֎S;L ]ge1Փ#֏۫UMXY$*XA Jkx~BޝН` NN@d]ev ףRIIhpi~հMp\gDm 5Jxw$:{Rx K5-ҷiDWهBHUdM& 6ku]#?Cx[ʍWsÛqj[yqwE0R 'K3rror' oc,.*6lbVe4gҎ;ؓe\#=h(D /Rs{/2~l<0ƀn&OFo'ӏXF LӐ㥸Sǖ Ew" #)\]~yj639O|ۀ"&Xb>oK K&%SUّN\h'f>gOrԻ#hk%9j6]tW*o;v?PO#znAXՕÏ=Ժ.U !oj=qdFug;lSDiA 8Ű@61Ҥ' <kf(ԭ厱düꥅPj\ڧt⦨n\Ej,pgi/;3r9ޘw+:]uc[?o&ݕ$MZ\X>>BbJh$G A:CʡXޢw {0 R;T~'M欙>uiw.E&("3-tSm-$Po'}ϳ)\ ^@wM(Ⅵf?W24zmSʀXZ Q{(R5M1`Z#!tz9X .bDta\;NO6z$#ur>WZiᝍڂ h=r@y*ܙ&21TN# ;pwp2^jOw40_XisM,=ˆRf&i=Ӫ99i "K{=WL}Cm>Dީ#2_H1[PXX^'"Qh&J]|q[m(;$)2`q *2r׎LdY/Ҋkg41n&kA饲`7fiĨcnkDͷLas@;M9_. y;G^9Vi35e8'牂Qf/Wxgu{;9ܻdk|,PZlLtiѴ)D]QJZ!͋t@C>%ilbd(qEt ٭QDwVٹPP|9;+:3e#_UY{Țys;w%\a5B' BCf)8snF}He׋lh/'D $ IG>쥛Iz<53 Fxo<68㖋x ;^?pB-lA_ӵFsjduszziI Iݳ`)ӎY'TޟMo>!2bϕ%qgbg0I-3q"IhOk0C3cU(c|Q6غ/߼pQҾ89 YیRNI@hi3ڻ8z0t/ן[iS+x^8nGҪi2"=3#@| YˈFZ7"C_R4r+=bn?E>I!< `ft3")B KdG-.0!I|~Q웃Q^J-.@'M 6JzOL ғ Sy k*GPd#p[sRQoTAxpVB(}pcq;6`bZiPM'fHQ[jLM*I%156 8@_xݒ)&F'bLNj﮶̦ИU$Y(3dW6aeM O&MTE]Z q(5BBQmsI:`oUCy8~cMQՙQ(%|S@pcoڡJar˙-)*Y2h`=n#N\%6vvsNtӕ;FB$xB1eN@K%^'t}o *|"VۋtܺN$ ;* >ndl*yZHyNPI+-xF؝s97㽒>K}^Owo82͟]Dn|FC=pu:<Ў(TEXXGSn5xxf_漁k4bё^ǎG?U s-O92VԹ:A3(`Hsl%cP7 ׁ'M1;e5hx|/PiT|yT:^Ox*{g nKKv|ˣ4m:w뷷} k81i3/- yD/E;_DMpI,W$^>k_&' DD[6N*0$&zܹ̈́L{ !L4; `zm?fsd.3`SU#!sb\!:5 $[:1^hl?Kly5\PH/hJZ5s:3Lp:KMI}:+^ ,vJd^R%h]'Tl,}}nIc! +PP:8ȔTښ"IYYADc+? 7Bc mܯׅmm){2KsjԡH[-gdHH4P mF1:"rkO(bH i#%9J2]&b whنgQeĹWdIwތ|1=!!gÈAz 1P{ԒkO9("j)¼/E`xqOo&4HOY2-DlYk(çvFWD*e2tD $`ԒDH t`Vz`P{g#3c/fVO 4]g /?j˼f ڃ=Fy* XW;zO5Z-8P=IT^ډTodw|@}] Obzu88M>$QuIX0CjErheIGUt;saPRYla?d>.8[EL)t6rzYsu(8Z򀜻9QƯa cdn-4TI0Ǖ뮐w;I@"543R8|5/{5!@fZ`CBwAkU1ʗWf"ci!s;wzRԌ#-mw9ۘG A^`a{ GD(XkORaӛZ&wP4Q?}XX KDaT+:\c#dxTTOJ̮E&Sb#W*cQKW_+<9"~jeyض~./Wgr1K>y&BF <wX.-bmR]N(w1_&A?!ԧCALNŸG/vK6whVf#_pBT|s[I#@η%q1#ebGIRڧ'j;!b]4ƫԜ*X[ҍ{ ܈"F #NudS-c[բ 5Ikؠ")*r$C_H sq趆_`j1Qfc{$n̜\#^]أqMvbwLYM~%qu$K-n~gwq[f-Mu V] E7Vg9#T{55 ^H\̮͓'kp#ߑDFm l E@<LreMơr<  UDѺ\S]+ZLi1q۾˼B!lqr!Ň~,^K#,]'Re4Zc|t Mr"J̤ ݚf]]qOǂ?hOE+u\F-b$1*Oe޺sqϽd V33뀪l)3S~^;~eꟜՑf\1zT!V Hl}dӴ,bAx4ȇ% eercS8mvbt]#>stB=>vlNNT Z%;^QAJ5?e:p0݃T >=44DVO Zv}"a7J/A3Ve'mHhz,I{pq+ND+cjXj-.爅\,Sa(vʭXqtCs7"L$.߷i~#ě^I; Y'Qnq&[L a>Q.8GIhxA{edFfT6>-yMcuH_bE<>5/5qh0 q6!s3$^kfP7ċby0jA7 {cP!Z-b!DpʕJƶ/јa^CySI{ iD="5&=àj]1 =mvP% *+t.Mbw:yG_ ~i1ĕc҅L4~g]DuuA@pؗRߜ[0y= /j+e^\h)މHd}f+=E%[̪[5*OŮr_G=^A-Aܯ6;76aY(AF{ VapoGeX;z蹉CD+𗀨*:eނ/bTTAО}/ ֮h4BrS=YJX]Q?R֝hy TSfLm!г?[HӚ2?=y8C<4iY1RD3ɘ {;LuaxYXT`gfmf|T+v_@vAz_cqĩ?VhY@T>7N%/pΜ)\0p \0+C$ݸT:k&@{qLz" USkAq-y %޸l7QZEHo<[ ǎLL6ܡ:Snk@^e)T:7v4 v@n5ܳloU=`FMFQRT-O,Χ3[|U)Po1h~M Q5&qKlC%`66EҷNí"/p})4h׊=|g*K! gT"\H7iU% lCo3Sum6JzFef/t;zs~29|QHzy^?=ؼ3P~?y8KM`H5{1Qǟts2Ӈ5<]3Ġez?$'7Œ<,ي!SC w'#ywRTSʕb&_]+ Qj'}KQ皉mG|k?Jx'>n'm_H{31' gNvV^FC288py>TXYCoMI\St a)8^i}W<,Iz<%&u{|nܼ\cK%rnd;?VKިkeEͲ%ƈrx~f  cd/RS .$T1車~T> |CVNj b'Ws#vozW"hⱜ̀R]M+k!SV)9߳_عBdh246~l! Ѡ®fY'kS knFgݧV%;SqgsNc*l_%g!TuoCAhl'@E";x/({|gd\*eW6;y6b9 fROA4TpX7+::Z]٥Ukt:< ]7)nu ՗ Yh";=,(8f$rE?}-0zv2waJ`/ͼL" gգE-W;?sY_Su̾3;I,}ݏbIE'(HuMȇ bX1JTgh ,~EmmVtA oww/XlkC6duZUdNG & XԨt܆Z`}}:|~ce]7{*0ͺ8:B;b#V,fV}p`WAH̹?>N#q-)MNMPn(BTQae8Xfxv5(GhክGvMlֱ֛o1U} `\.rD8`/6Sܹo!_S_LD7H;j{ƍCLI)ϵHnjSMNk5oXN:քS6Mo\FNf۶HKx| իT:qޗ^RΙz;C6 2Oo}#Q6 *f9+p7)+ז|{("kӍiq7W67HF<(߳n|4{( f67m^I3Ry#krHwVx'@u/. \>k/q:`?1Gld 4'yM+~=j۷)rTJOeJ.Et_30' ; ynr4+70<&=-DbC:H+Yd7_8CO&r\)*OӫWD"k v>-cj(=y1cJ6do;+AC A% ڑCao{DwMXPQ#?ȟ`GH߹([BlΊZ}`L95e|򋈗V שrŧƟؙ#x9Xr IqQ&k/(_DHПD3z^IvbL>F^:Cz+ 5.Zo !T6MSX&i5PD%NJ]?<$A.Ao/V~J[emϻStXP@~t Iȗ]ֹTz.ͭ2@)ReAiZC:]msl-׵ KYBZh'q烷WPQ~Pހq qW4ls)N%=_ ahg#r^&+ɻr/&;!Bɒtr:hm0YM |)eFX<TWmwqzC,4(%6JEƶ5m۶mضm6fbv2Nr˭j%l)<*k?Z@!F.hLp*4PdGw< IFR+'Y|XXbt_TfU]6Y:ߍ o MjUz-{P!Qcdz2k-<7[f"֘ttbuP(%e?*GB$ uXwSmjPFY[mlL spgɆ`u9Tѵԣue҇Sbe{}|vBGVYiˁ#u=^Ndkᣐc.y\թ8+#E%s[_xe^O݉0QT svD$Wt|=vSα:Bƨh*SDFx jH;aV֠-{E>"/2Mhuzլrv?g4H0 |=t|~U m:Wo QB:(CnZb;/}AwNNq)$t%vǶ[BmXnd;XH 5aYC62:{Ua[D]DjuR*r~̬]]0*}J:ב`Xcذs5,Nؕ#|oȳȾf_O̠J۝ىs/ 3UBEړ×1^Dԕ}1{,k2/vnkv(1G S?a1D)Vۋj4x|l|"/F$<)mq$mcW"-*  5>:+S鏹z0E~DQ:y%]+N%&e1@ǿ0)c}Z C5i>2D7LI <6(ter`g,+@b\ԅdA֦*w#?JIZDNwt:q)x3K J`Kuj_tIBx \q^fLn&H[;@},~%hk_:+C mtg-1In'ga4WCfI? gědqH:6u +  Y/h6Oſ!UvMf.~d%?=;h>|S̗ 7;0 ^!(I3gIJf,lX$id{oxU~D ӏHhˆ։i5Gd`H2Ij̃ipRcyyȿyJh7n?|Ga^+@V[M(* 颷kiq &3nDdz``ףylكz&!Z>( 7lب*SxN6%\ hǧA?CɈ 迋9&yB#BQL-xuz !ڕ LkYlѺd~sWfWz?Mel2|X FUZOdwG?tf>OɫUZ <jK~C{hu jg7XK?T h~ [2ߑK/kݹ+k=I1( _Tu}ILt&R漥5/͸dGiev傞)RH{`GV ~w>g5>PȈ-ž!7Z]/qèzJ1 Bqd|Ǜ+d!H}ɳU%{ȰL'D=aar 0{տrg:+ѨJc(H[y>H(jwg/5[qh+a9[Ì= 2gm:~s%!dE /EȏVSͼ#G4:Hm&BXYzڇ)19Qȍa63D8Fi+߹/ ls-SƫY܋&&G5QArC {ڙv5QMk h7Uh$z]*Vlϑ.Y\MJ?CTX-]Q6ˌ'?g<"eU-@]PtG4;mTyĶ44k\#JMD1Ŷ=UEYxY<;6&Duk Rs,&\;6:󉁤*!4Pىذ)hMAyu u0-sJڦssO!IE K.,O%ޤ\w뻧 %ؑ9㧘|!e X@ ewXncJJ%*ӂqx"$!Slidru+oKrJb Hi,T$>m􏥙*Fе_~꼭Xܺj_E[8Wzsz-K`8x49r\Vُx_02Jm\9|64`C\j,63\+Q R[ys .8!BW%pE)8aƝaaڥ(ImS͉ ڨh[NEu۽ٛlj̜~98m&"6 %b iE+Rc oWMt?f$`]Ͼw\]>9 n!-8 z!VRԲI7{Շf:Q+ԆD`e& fŴSGNU}2&jR&KN& Z[;9z<*fYSpfS0N" t<;|(۶]{,,s(^S.֡y! cڕ)o(@/_I3+I*[Y-R)`)a:&sïR:7u Kjl,:89E zhWS =n'ol>t#1WL +H>lqhM>۲J͗Q:% Hw)7jb|c~?0Vr@#0#gn) 9n^R ڱ&)c~ka<>-.҉"RUMAtֳ]L.̾+ {\\ $Xk$f;]sPITqXJu2B:sU[_<(Ac 6ԁ>rjr%#웩Y+ʪpGK<aSeR7c|6nXڸ**F{ UTA82nʋWn(o.3([O$Qd;m:W\qz TGLZbzQT̚ l &>Bs7Ӟq14 z&#Bmj+!\Fȷ{R,?'r^!aRrAebG7ڠ 5n6@4grk9zj9W8HqpSk; a^6`+Lccs*k;kftN "G |Dvxq҉7'&iЯ5W'Ѣ,P)ZsZl׏;XiwzFrK+n҇ߵ7|y1!/3G{;iMh:NRuzHes) <;vFp}LP[=D/T_WƂ;Hpw{k|mY.w̤sb9HTf`>ҙz&kBbNngo TS I-$ꁲv.dt.J ;o!)6?ppsf'&A+jهLϘ_ow4}ԳGрOɲ5X4ڍ#lǶQ@5K d$&LErq- Ϫ~~ggy70|RF>c Ppm+#5qu= jcjVX{)F=5FR[r]Hz󸫢4JqIJ"}^| ĤGP:2jsEMhhFN1ܵpsrܼD"D??{0iJ!ҠDqiID66f导a+:*1r4Mb`^߆7?ף,(y#02졄Wc/,'M W IϕtɵȩLdJ2P݄x;S!v^I<6k$t nmGۺv7:7i0bw}*x6 pŻǝΧ*.8UB.(/f)eRgirzilaU/ڝq:Pƿ$Ɔ7Mk`)SuM}5z@yY"BLb_\D:e2@fܞ"b8kgSa-[VH?*WZOGQtb=ۮ~8nba2kr„' MUJDqgCIL;kG3#<GwfsIo tD)AA М/ :dj?ڿ.S'CUG@b| h|aM Nf#ƹJniUP|񏅺-4P`ؚiuL0 O{mĪiw_p[]!jT_@ohq8N?s- d7E۸4446A7cFk͌ӹ9SONKz‹U"lnVHةh)nUAQ}C[yiazQWnA~وJhL2 ݱg(?U۲YMV8g=Pw1{.`i"ћVjl垧 m֧5jf4oD3>g]tDD߂ț^zr\¾1RZ!B2-h&Je ^8v Kf'q[/kZ & Z$Zu`̊1ML!PS"W܏co_cO#M|D hB-XIeMZtʺC_d_xzC2/`J&l18\Tcn9Ef6t  QXcy-+"rꦮpm_qS}t >*Ad59aJe-Xp\c>4:L Bd---:[|_z*K$eJ?,=ul`#/S[uT<ϛڔ?eP>|wsYZʗCOZV,f@G .=$ш~И 4s1QwdI'TYFbaצxCl^c-WcLjQ%}(V6na\{j1ը04fs }|aE,Wx:I(PQ."15pj/։|Du !qNJHTm0MfȧhHڻ[ޝ/K5$stbDYY`坕 2RTq1*:]aq;"o8_cpw)[M㵹d%N:UsVk* Kӷz2ƐOvIEm#aphBт˛N(QQqq9Kה&C .~}v릺BE<5ⵇc{@Da)JSpY$֣ lXԑ_׍GTol^ չtz=EV,zmPS7:d<8 Uфe,LD-+!{l8X_wHXqCd(ΐ梼V!wG0{’8sk#ӡȾДiM7zeln $RŽl) vo2%h^O(9iZ nr!l%FA ׵9ăƅ: jEf'mXv%ȲewUqag H!NvuϷ(Р1̣$=5 lMphŤĠяRW'ٯUԧpT:>+?̣ހOA[x6!oM]`m.σ(B#IfƔ(*6B92')~m5S*vpW&[)^ m"ZHű=dG+ر?ʳΞl㫇LNLdIePס8e~`ȸ"cCh.,l+`m4?69{%\& $μIΑ9{?]7rPbeb^~њW]ȶ&؜:2wџ$ޘGaBV}PA]H٭ZQU)4YuƳ!$',H(Wj#MG֫ M) UKQ&a/. |wqlj˲?jt`' ^)?\D芒ջD3\𓰆heL?k4W/!AkΖ ٮQxyB_-r˿*s7VVEQ9R 0bY /,t0oTBYB졎3FAB 酔(ހi)/vH>Ht71H[~j<\)"Pa++oRgkafApR:F{֘r f{@VtEtQQ~a90% ײdkė8.lX!ܷX_TJAyRxŶrb:w ^;͚CdyJKm멗A1'K7"7kJ:R!f1:hW L.-xu=)z=Bb<gn"sqr#' < r?@\0D+ `Rv$&-HH j!tiWx ZMdɪ;:!՗Ĺ"%U-=U<3G{˷=.O/=NglOkܬKaO7Aьp4r-ѡ! tOSkɆ'XM_hyPʣ3VlCIhB[,H&-V;cs'=<&CP8Jʟu"3" mMٜFhJ~nffwX! g:K+3JQK2)9ڭ{_CB<_QtT4]Y4+ÈP Z\ =w!\8QLb5-jdߧ]nWvi8 鳗:*JL*!\4rO6p&)*- 7]:Om4˴r^UcO]0@F9p(`,+ޮuYlWjx]P9pLvƔWH@ *t5EERkc*._SŸN:s|D"ᨋgahcM4 $c抨7?"0&;ܸ.N>_)Qs75C#_*f؁ -+)'EȈէ܉g( <=*0ƟAs.()g󓻩~Eo-.HxImmgH滜X;& ,e벼4դTzlzpp-0 { =Y.2=JN o:i2]G:A 5@;+ гtlI۷DY^M7i3 _S0@*:mAWѨcBOLwbjZ/t`4`~d؂[|oP8x1h?#us 1 ?+1,+ĩǷоX.0`r.}{KzZF&ĈdU^az߅xrkzi}<>?P4>3hMg 3sؑ&M:L xn&Ɣ27p-.%KRݮn 6*ύU0`[r (U3ǎZ7r(^/GtRDv8ԋvNh{[lK endstream endobj 949 0 obj << /Length1 3388 /Length2 31368 /Length3 0 /Length 33164 /Filter /FlateDecode >> 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 951 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

|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 957 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 961 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 963 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 964 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 xmTn0+Cl M8(z%:K$9-gwCro~<'im$ ݱ$977yW]}^sUk7lfU[kwf}Oidco'%ȯI߮ _~~hA{)mu{lc')bzkں?[ uS/$?W  1}ƻ`׮o7qU_ZwB˥W의~ oźvaS~Ӿ`!R,ro-D A '<6sF )1!Pp #]gQxVT'RFh-յDz<gcYvDz~ !G9Kûq^YL8}'!xM.E?q癊2 ɺzu endstream endobj 966 0 obj << /Length 700 /Filter /FlateDecode >> stream xmTMO0WxHp(6qPU)·hגl$$=o&ˊCg{~>{0cxyM ֐Ҡogo-!Lr/ǔ&XXo᝵|6(7{)sxW2.E ńթ1DOi:vGdяFy Mz endstream endobj 918 0 obj << /Type /ObjStm /N 100 /First 939 /Length 4210 /Filter /FlateDecode >> stream x\io8_("އiEp5ֱ;iLG~HY$y/ڎJZWPU(Z*\pE02+[Q=w4KjKsB|?}xEW#M|-nIBFG_!A* LWAsYcu 5\z!INZt1ힾ-ȉditVWD-*Aŷ-}@! !:B$ 2InX@hӚuh}@ TSKA7.ЗSgk 7OQ_t 745筑_sߥf~HW4$Ҵ`D<7H`&VQ_(͛* hLQYC&FHR?QLRb`³p/q8ϡ2yji%֏7k{d%]@2;\ W*F#٨-.XuH>i*h̘yUAFț*mey_88+#Du#/n5I}&za4{/S*5Z+2M#v+gV[k-+%hHhA~U)xQy #[XHCZ=^ 6̣ٛu{o p;/Л'Vȱ*"4f\:>}i6͋E#OoWrptL?ّ޺sh}HEzZ赠=֧֚/.azv2~O<;={dp48:>p[Ab?ԛJfbz=A|&͈a$وPw lM>wA=|ۗNE/婐:9 7 sʚN9aϵ8iB '-olPbRؒalZ?ow|Z Sd Yfwu`rqGlƹgHuJj-QP*Ha ZF(d lG̷Ն8վ>ޓ[f)px2\^ΖuD7P_O'#D\Ǘ x6\H|]΂׳5ƻ͑u8/zkˡ=~}`g%U>^kRBTq5P*PU-rudMy L8 v@H,} ^/[p3lb0hJlQT"Y;l+Z-mcQtQ؏E4rdzZҲSr MeȾG7a_w:|ر7;Ngs)TǞ,.=JeР/㶧g ^6٧M6h|A\L˾^u'Y/f$ *zZO!yT^ۧך^_e?MH}blG\4j W6K%ZB OHoW{G][Pr RɮhF?+d݅ ek%s( 3_W [퀰^VGrxrd,H߲?rsAKܒJL};SɊ_ ыY=C0b)US0ɟLB*ko<*~D_Mpn|'x\xa$}&Qv:Rlf{OKWiO{O{gU3{0<È4QV&8|y{$\BJ83 &> endobj 972 0 obj << /Type /ObjStm /N 95 /First 833 /Length 2643 /Filter /FlateDecode >> stream xڍZM7c:@<1Xl,6Y3 ˧jJȩY,6Z-j1*,C/ڛE'ZiܢcZlT[#Zb=KCb3-=-Zi6-_'Ң+}9YVެp҆f +F5+S[D󬜠X9Aエeh{س[8*c=Cf=g sQqT&شpT/(`1pP&{Lb9dU2 F'i?YƺİYϳ3Ų-gq)ƑEXe^CwI,nQ%1.9uBx)2t`SC[lMxRaCVaBt3b5әGtJI/!~s=w[ ϼGF>BPr8ȵR Iq (NbDR,1DY1+Y\$\ISge{\f=i'Ɏ`F {+S֌ׯOw~~p5/r?/y!ߟ/ՖqEIo-|eFn}ekC)l-?e-=-=SekGR7[eb(VU)D–آ=M'+l=M{qO0>i}(܇קÈPD2fsk nn6xvӾKV|ӏ%^Ϟv"~_hjKa˕<8;%FYq%+6lYh ¶mp5` i;b MjM&n/vFGSrbdN݌5% :M"f]r̔H.)zbJ (C̔(~=*~UV3E16n wӄrt閞&|;귥 ע׎Zmq³h.[,MHv`K Ǣzki4XQ[-]5L(-uQKC EoYb_Sf'0tM?8?9jG٦-{[Aަ$ݕ6t:Qs*=i_eMVjGXJldmEtQ,mK4DŽq&Ts p؊8z`GVay[n̤kB]GUNqSƅu!}tyY~~><_ϋYkk˧W̕] Xl%pJ!q]] \/YM,2'%-J si!ޕ4kWA 䗇LQ@$Z̰x Q#xp{밭7 $G촩%lI'BjIbr{zC0Aո Ҽ6]O9/t@vqM uP͎;$tNNcX371, rR$pt^f-$5Px-t<$4MXu]Xf"$*mvW#u *4u{rLwuH5vz|!0N$޴k>*ރxDn'=.:1.zDy#*3=ATLO qۊd`LO0`v@Oi`z :@Ƹhq'Vqy GB=b 8w0xi=jzT"Dj}:ҹhqC&<$uC"KI,dbz-9Zz#C"%QCRo >2=QaI[ţVAY|$$,zKy{ b DFke@@,bYl)Fc, p,y4ibBc IAicĐCmIbGPM(raY D r-n$L| 3)KS1"Q`Ln/DqОԁܝ:0`f0BQlkn-nR'Yj Fō!n-ΌޓǃVm0` ǨV>$VwBr+D:MhL/!曘:pyi8up6kYM'ڈGW R8f炔IMo0T/1TČNks~E$g z#MwHqL)t,=Ng=A'>v.غ;x`qx5c'|Ww>Jkف)ݖ˯\5b+EIexF#A U$T'=;8ub4vb5ͯ >fcqb+E:"9mtD7R}IF,aL/?cʹ۴O6,mq'ɻn||Ji!m5yO\֪>ot;̑6R|{xo/Fyƕ&;ߞycaގi;mz0i@&yo _bP;~_=G~\^X1/I/ǧYbXKʇOO]rٵLJ^>~}맗__r{K1\>A/f屗 endstream endobj 1017 0 obj << /Type /XRef /Index [0 1018] /Size 1018 /W [1 3 1] /Root 1015 0 R /Info 1016 0 R /ID [<9040BCC6C05D161D09058F4C1D1A96A5> <9040BCC6C05D161D09058F4C1D1A96A5>] /Length 2476 /Filter /FlateDecode >> stream x%YlW盱b>qۉxgxN8b;+* $ A. ()-jҢ")f/@B"DZͣ9(-ʢ8Ȣ+q}hb[h PðEh hhRZ`'Z%Z']`7Z Z2PPv(JЎ0{΢15hZւ:Y*a`!ASA m7Z3CMBC)h n MEkAP/M؅֋PF;RzΠ1TѦJq9`mMFIp &Bq2ۆQh GۅVpLUg94yFF}f,~ σ9&4YG;EvM]KhGJ^Y~\AF*XAArhQupmMKmDc5&&)4-[`mlt }N-hhJ\c,rZ<61L_=9$v!oיy4Yo\H,}pk%sP KE5:oqDP1J)̯Uu%Eխe|D Ԣd,LܩVTz!Ik&=X)SDZeQ= k&%0q,t`t.L.`ܢ9NhlQҮy,M׀"-Zc͒|E֟ϑc `(8bՈĬm,I[r_ Ű Ѭ=@FE )0jA&A=ЖYۚm/o R}ɢO䏀VpآoR<9fuHv@wz1:cw_`U%^7t'i0dO^y Y/԰jF8c_y`LAެ$8-Ni3 `΢-h"X2XfGtp\J< f*7L4kZY<@GŠQ ^U3|T3RM50Mrխf6UViVMT _efZ.+ɰY`4&/. eX*;d_Ic1p6gf4:4{IT3n,IX*7{?Ұ^~7aL=NaPZ9f΂ Pisf.2<r4l-.ݺ+̐alJğc*XW5&[Xy24kL:BGݡMlF4FZ J@ ok׫> 7S2uYeߒK}Ou ?~O@ hK[_ tVzA&HAc`Y51N!_/g,~F ISW8. ~S 4Ud*VefDUJP}uRT7A!ҿzhQ<^G6P o_,󬆔=!g;k P. vX+:] Yh3*@% >A Hàޒ=#:e?h"4V@C;hQ~>l:,iD'9xB%:`p0dəj0 NSڂ0K, aԒ"]r?LA%u  ڎ]X `(,Z^PapN%Y`ŒS'@OP%sVٴ݈adc(tǎvؑ͏q•@2-oteP:GG]-tP8KGv@nDZk8K @hj\t6Vs:o5Cq40ñM8zGU(keq`#eNt4nPE.-%@bK~Y@ty6w,+b )T:u߭H!n+o>xO?+xOJ '{<xO='{K>nrUGvJwq'[xl᱅^*&[xl᱅o䉧uBQ%o<.+jYumE}ZG=V\s_GV|9Y%:\{:/[ѠZGCԋ>s=UA'5{Yn,IrcQn,IrVSES}*R5꘱(W+>+aTRhJX4ՇH~%>`[گ~+KRI@;S.+IgK $[S8 endstream endobj startxref 435527 %%EOF ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3731167 JUBE-2.7.1/examples/0000775000174700017470000000000014626040416016251 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3731167 JUBE-2.7.1/examples/cycle/0000775000174700017470000000000014626040416017350 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/cycle/cycle.xml0000664000174700017470000000046714626040416021200 0ustar00gitlab-runnergitlab-runner A cycle example echo $jube_wp_cycle touch done ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/cycle/cycle.yaml0000664000174700017470000000043314626040416021333 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=1717059854.3771167 JUBE-2.7.1/examples/dependencies/0000775000174700017470000000000014626040416020677 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/dependencies/dependencies.xml0000664000174700017470000000125114626040416024046 0ustar00gitlab-runnergitlab-runner A Dependency example 1,2,4 param_set echo $number cat first_step/stdout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/dependencies/dependencies.yaml0000664000174700017470000000071514626040416024214 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=1717059854.3771167 JUBE-2.7.1/examples/do_log/0000775000174700017470000000000014626040416017514 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/do_log/do_log.xml0000664000174700017470000000072014626040416021500 0ustar00gitlab-runnergitlab-runner 1,2,3,4,5 param_set cp ../../../../loreipsum${number} shared grep -r -l "Hidden!" loreipsum* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/do_log/do_log.yaml0000664000174700017470000000056314626040416021647 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=1717059854.0 JUBE-2.7.1/examples/do_log/loreipsum10000664000174700017470000000067614626040416021550 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=1717059854.0 JUBE-2.7.1/examples/do_log/loreipsum20000664000174700017470000000067614626040416021551 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=1717059854.0 JUBE-2.7.1/examples/do_log/loreipsum30000664000174700017470000000067614626040416021552 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=1717059854.0 JUBE-2.7.1/examples/do_log/loreipsum40000664000174700017470000000070614626040416021545 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=1717059854.0 JUBE-2.7.1/examples/do_log/loreipsum50000664000174700017470000000067614626040416021554 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=1717059854.3771167 JUBE-2.7.1/examples/duplicate/0000775000174700017470000000000014626040416020223 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/duplicate/duplicate.xml0000664000174700017470000000130514626040416022716 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=1717059854.0 JUBE-2.7.1/examples/duplicate/duplicate.yaml0000664000174700017470000000076114626040416023065 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=1717059854.3771167 JUBE-2.7.1/examples/environment/0000775000174700017470000000000014626040416020615 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/environment/environment.xml0000664000174700017470000000147414626040416023711 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=1717059854.0 JUBE-2.7.1/examples/environment/environment.yaml0000664000174700017470000000110214626040416024037 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=1717059854.3771167 JUBE-2.7.1/examples/files_and_sub/0000775000174700017470000000000014626040416021046 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/files_and_sub/file.in0000664000174700017470000000003614626040416022314 0ustar00gitlab-runnergitlab-runnerNumber: #NUMBER# Zahl: #ZAHL# ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/files_and_sub/files_and_sub.xml0000664000174700017470000000207314626040416024367 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=1717059854.0 JUBE-2.7.1/examples/files_and_sub/files_and_sub.yaml0000664000174700017470000000143414626040416024531 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=1717059854.3771167 JUBE-2.7.1/examples/hello_world/0000775000174700017470000000000014626040416020563 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/hello_world/hello_world.xml0000664000174700017470000000077414626040416023627 0ustar00gitlab-runnergitlab-runner A simple hello world Hello World hello_parameter echo $hello_str ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/hello_world/hello_world.yaml0000664000174700017470000000044114626040416023760 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=1717059854.3771167 JUBE-2.7.1/examples/include/0000775000174700017470000000000014626040416017674 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/include/include_data.xml0000664000174700017470000000052214626040416023031 0ustar00gitlab-runnergitlab-runner 1,2,4 Hello echo Test echo $number ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/include/include_data.yaml0000664000174700017470000000027014626040416023173 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=1717059854.0 JUBE-2.7.1/examples/include/main.xml0000664000174700017470000000133014626040416021337 0ustar00gitlab-runnergitlab-runner A include example bar param_set param_set2 echo $foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/include/main.yaml0000664000174700017470000000074114626040416021506 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=1717059854.3771167 JUBE-2.7.1/examples/iterations/0000775000174700017470000000000014626040416020432 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/iterations/iterations.xml0000664000174700017470000000253414626040416023341 0ustar00gitlab-runnergitlab-runner A Iteration example 1,2,4 $foo iter:$jube_wp_iteration param_set echo $bar echo $bar analyse analyse_no_reduce

=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 953 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 955 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({

jube_res_analyser jube_wp_id_first_step jube_wp_id jube_wp_iteration_first_step jube_wp_iteration foo
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/iterations/iterations.yaml0000664000174700017470000000163614626040416023505 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=1717059854.3771167 JUBE-2.7.1/examples/jobsystem/0000775000174700017470000000000014626040416020270 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/jobsystem/job.run.in0000664000174700017470000000034514626040416022177 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=1717059854.0 JUBE-2.7.1/examples/jobsystem/jobsystem.xml0000664000174700017470000000376214626040416023041 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=1717059854.0 JUBE-2.7.1/examples/jobsystem/jobsystem.yaml0000664000174700017470000000313314626040416023173 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=1717059854.3771167 JUBE-2.7.1/examples/parallel_workpackages/0000775000174700017470000000000014626040416022606 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parallel_workpackages/parallel_workpackages.xml0000664000174700017470000000111114626040416027657 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=1717059854.0 JUBE-2.7.1/examples/parallel_workpackages/parallel_workpackages.yaml0000664000174700017470000000064614626040416030035 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=1717059854.3771167 JUBE-2.7.1/examples/parameter_dependencies/0000775000174700017470000000000014626040416022737 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parameter_dependencies/include_file.xml0000664000174700017470000000043614626040416026106 0ustar00gitlab-runnergitlab-runner 10 20 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parameter_dependencies/include_file.yaml0000664000174700017470000000025114626040416026243 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=1717059854.0 JUBE-2.7.1/examples/parameter_dependencies/parameter_dependencies.xml0000664000174700017470000000177314626040416030157 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=1717059854.0 JUBE-2.7.1/examples/parameter_dependencies/parameter_dependencies.yaml0000664000174700017470000000157214626040416030316 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=1717059854.3771167 JUBE-2.7.1/examples/parameter_update/0000775000174700017470000000000014626040416021573 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parameter_update/parameter_update.xml0000775000174700017470000000206514626040416025645 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=1717059854.0 JUBE-2.7.1/examples/parameter_update/parameter_update.yaml0000664000174700017470000000135014626040416026000 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=1717059854.3771167 JUBE-2.7.1/examples/parameterspace/0000775000174700017470000000000014626040416021245 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parameterspace/parameterspace.xml0000664000174700017470000000121214626040416024757 0ustar00gitlab-runnergitlab-runner A parameterspace example 1,2,4 Hello;World param_set echo "$text $number" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/parameterspace/parameterspace.yaml0000664000174700017470000000067714626040416025137 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=1717059854.3771167 JUBE-2.7.1/examples/result_creation/0000775000174700017470000000000014626040416021453 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/result_creation/result_creation.xml0000664000174700017470000000361514626040416025404 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=1717059854.0 JUBE-2.7.1/examples/result_creation/result_creation.yaml0000664000174700017470000000251314626040416025542 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=1717059854.3771167 JUBE-2.7.1/examples/result_database/0000775000174700017470000000000014626040416021413 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/result_database/result_database.xml0000664000174700017470000000223514626040416025301 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=1717059854.0 JUBE-2.7.1/examples/result_database/result_database.yaml0000664000174700017470000000150314626040416025440 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: "NUM" key: - {primekey: true, _: number} - {title: NUM, _: number_pat} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/result_database/result_database_filter.xml0000664000174700017470000000242414626040416026646 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=1717059854.3771167 JUBE-2.7.1/examples/scripting_parameter/0000775000174700017470000000000014626040416022313 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/scripting_parameter/scripting_parameter.xml0000664000174700017470000000213214626040416027075 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=1717059854.0 JUBE-2.7.1/examples/scripting_parameter/scripting_parameter.yaml0000664000174700017470000000137314626040416027245 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=1717059854.3771167 JUBE-2.7.1/examples/scripting_pattern/0000775000174700017470000000000014626040416022010 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/scripting_pattern/scripting_pattern.xml0000664000174700017470000000371414626040416026276 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=1717059854.0 JUBE-2.7.1/examples/scripting_pattern/scripting_pattern.yaml0000664000174700017470000000242614626040416026437 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=1717059854.3771167 JUBE-2.7.1/examples/shared/0000775000174700017470000000000014626040416017517 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/shared/shared.xml0000664000174700017470000000114414626040416021507 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=1717059854.0 JUBE-2.7.1/examples/shared/shared.yaml0000664000174700017470000000060314626040416021650 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=1717059854.3811166 JUBE-2.7.1/examples/statistic/0000775000174700017470000000000014626040416020260 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/statistic/statistic.xml0000664000174700017470000000273014626040416023013 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=1717059854.0 JUBE-2.7.1/examples/statistic/statistic.yaml0000664000174700017470000000165314626040416023160 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=1717059854.3811166 JUBE-2.7.1/examples/tagging/0000775000174700017470000000000014626040416017671 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/tagging/tagging.xml0000664000174700017470000000145414626040416022037 0ustar00gitlab-runnergitlab-runner deu|eng For german strings For english strings Tags as logical combination Hello Hallo World param_set echo '$hello_str $world_str' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/tagging/tagging.yaml0000664000174700017470000000107614626040416022201 0ustar00gitlab-runnergitlab-runnertags: check_tags: deu|eng #check if tag deu or eng was set forced: True tag: - {name: deu, _: For german strings} - {name: eng, _: For english strings} 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=1717059854.3811166 JUBE-2.7.1/examples/yaml/0000775000174700017470000000000014626040416017213 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/examples/yaml/hello_world.yaml0000664000174700017470000000072614626040416022416 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=1717059854.0 JUBE-2.7.1/examples/yaml/special_values.yaml0000664000174700017470000000116514626040416023101 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=1717059854.3811166 JUBE-2.7.1/jube/0000775000174700017470000000000014626040416015360 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/__init__.py0000664000174700017470000000143414626040416017473 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 . """jube package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/analyser.py0000664000174700017470000005515114626040416017557 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 jube.log import os import re import glob import math import jube.pattern import jube.util.util import jube.util.output LOGGER = jube.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"] = \ jube.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 @property def reduce(self): """Get analyser reduce""" return self._reduce_iteration 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 = jube.pattern.Patternset() self._combine_and_check_patternsets(patternset, self._use) # Print debug info debugstr = " available pattern:\n" debugstr += \ jube.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 += \ jube.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 = jube.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 = \ jube.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 = jube.parameter.Parameterset() for name in result_dict: resultset.add_parameter( jube.parameter.Parameter.create_parameter( name, value=str(result_dict[name]))) # Get jube patternset jube_pattern = jube.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 jube.conf.ALLOWED_SCRIPTTYPES: new_result_dict[par.name] = \ jube.util.util.convert_type(par.name, 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 = jube.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 += jube.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=1717059854.0 JUBE-2.7.1/jube/benchmark.py0000664000174700017470000010432214626040416017666 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 jube.parameter import jube.util.util import jube.util.output import jube.conf import jube.log LOGGER = jube.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, tag_docu=dict(), 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 = jube.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 self._tag_docu = tag_docu @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 @property def tag_docu(self): """Return dict of tag documentation""" return self._tag_docu @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 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 = jube.parameter.Parameterset() # benchmark id parameterset.add_parameter( jube.parameter.Parameter. create_parameter( "jube_benchmark_id", str(self._id), parameter_type="int", update_mode=jube.parameter.JUBE_MODE)) # benchmark id with padding parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_benchmark_padid", jube.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube.parameter.JUBE_MODE)) # benchmark name parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_benchmark_name", self._name, update_mode=jube.parameter.JUBE_MODE)) # benchmark home parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_benchmark_home", os.path.abspath(self._file_path_ref), update_mode=jube.parameter.JUBE_MODE)) # benchmark rundir parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_benchmark_rundir", os.path.abspath(self.bench_dir), update_mode=jube.parameter.JUBE_MODE)) timestamps = jube.util.util.read_timestamps( os.path.join(self.bench_dir, jube.conf.TIMESTAMPS_INFO)) # benchmark start parameterset.add_parameter( jube.parameter.Parameter.create_parameter( "jube_benchmark_start", timestamps.get("start", "").replace(" ", "T"), update_mode=jube.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 = jube.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 jube.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_analyse_data(os.path.join(self.bench_dir, jube.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, jube.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 = jube.util.util.id_dir( os.path.join(self.file_path_ref, result_dir), self.id) if (not os.path.exists(result_dir)) and \ (not jube.conf.DEBUG_MODE): try: os.makedirs(result_dir) except OSError: pass if ((not jube.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 jube.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_benchmark_configuration( os.path.join(self.bench_dir, jube.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 = jube.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 = jube.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=jube.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=jube.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") jube.util.util.consistency_check(self) # Create benchmark directory LOGGER.debug("Create benchmark directory") self._create_bench_dir() # Change logfile jube.log.change_logfile_name(os.path.join( self.bench_dir, jube.conf.LOGFILE_RUN_NAME)) # Move parse logfile into benchmark folder if os.path.isfile(os.path.join(self._file_path_ref, jube.conf.DEFAULT_LOGFILE_NAME)): shutil.move(os.path.join(self._file_path_ref, jube.conf.DEFAULT_LOGFILE_NAME), os.path.join(self.bench_dir, jube.conf.LOGFILE_PARSE_NAME)) # Reset Workpackage counter jube.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, jube.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 jube.conf.DEBUG_MODE: title += " ---DEBUG_MODE---" title += "\n\n{0}".format(self._comment) infostr = jube.util.output.text_boxed(title) LOGGER.info(infostr) if not jube.conf.HIDE_ANIMATIONS: print("\nRunning workpackages (#=done, 0=wait, E=error):") status = self.benchmark_status jube.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 = jube.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 = jube.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, jube.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(jube.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(jube.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 jube.conf.HIDE_ANIMATIONS: status = self.benchmark_status jube.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 = jube.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 = jube.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, jube.conf.CONFIGURATION_FILENAME), outpath="..") jube.util.util.update_timestamps(os.path.join( self.bench_dir, jube.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"] = jube.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 # Store tag documentation if len(self._tag_docu) > 0: tags_etree = ET.SubElement(benchmarks_etree, "tags") for tag, docu in self._tag_docu.items(): tag_etree = ET.SubElement(tags_etree, "tag") tag_etree.attrib["name"] = tag tag_etree.text = docu 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 = jube.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 = jube.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 jube.util.util.id_dir(self._outpath, self._id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/completion.py0000664000174700017470000000637214626040416020113 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 jube.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 = jube.main.gen_subparser_conf() all_sub_names = " ".join(sorted(subparser)) parser = sorted([opt for opts, kwargs in jube.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=1717059854.0 JUBE-2.7.1/jube/conf.py0000664000174700017470000000465314626040416016667 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.7.1" 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/source/version" UPDATE_URL = "http://apps.fz-juelich.de/jsc/jube/source/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=1717059854.0 JUBE-2.7.1/jube/fileset.py0000664000174700017470000002627214626040416017376 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 jube.util.util import jube.conf import jube.step import jube.log import glob LOGGER = jube.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 @property def file_type(self): """Return file type""" return type(self).__name__ @property def path(self): """Return file path""" return self._path @property def source_dir(self): """Return file source dir""" return self._source_dir @property def target_dir(self): """Return file source dir""" return self._target_dir @property def active(self): """Return file active status""" return self._active def create(self, work_dir, parameter_dict, alt_work_dir=None, file_path_ref="", environment=None): """Create file access""" # Check active status active = jube.util.util.eval_bool(jube.util.util.substitution( self._active, parameter_dict)) if not active: return pathname = jube.util.util.substitution(self._path, parameter_dict) pathname = os.path.expanduser(pathname) source_dir = jube.util.util.substitution(self._source_dir, parameter_dict) source_dir = os.path.expanduser(source_dir) target_dir = jube.util.util.substitution(self._target_dir, parameter_dict) target_dir = os.path.expanduser(target_dir) if environment is not None: pathname = jube.util.util.substitution(pathname, environment) source_dir = jube.util.util.substitution(source_dir, environment) target_dir = jube.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 = jube.util.util.substitution(self._name, parameter_dict) name = os.path.expanduser(name) if environment is not None: name = jube.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 jube.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 jube.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 jube.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 jube.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(jube.step.Operation): """Prepare the workpackage work directory""" def __init__(self, cmd, stdout_filename=None, stderr_filename=None, work_dir=None, active="true"): jube.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""" jube.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=1717059854.0 JUBE-2.7.1/jube/help.py0000664000174700017470000000316214626040416016664 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 jube 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(jube.__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=1717059854.0 JUBE-2.7.1/jube/help.txt0000664000174700017470000011441114626040416017053 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. * For the logical operation 'exclusive disjunction', i.e. 'xor', the character "^" is used. Example: "tag1 ^ tag2" means that one of the two (not 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 * 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 and if the "title" attribute of a "key" is set, then the value of the "title" attribute must be used in the "primekeys" attribute (and not the parameter or pattern name). 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. **Important note: The use of the ``database`` attribute ``primekeys`` is deprecated and will be removed soon. Instead, use the ``primekey`` attribute of the ``key``.** * "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. * "" can be specified in the database result and must contain an single parameter or pattern name. * "title" is optional: alternative key title. Used to define a custom database column name. * "primekey" is optional: If primekey is set to true, the key is added to the database primekeys. (default: false) The "primekeys" attribute of the "database"-tag is deprecated and will be removed. 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) 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 * "filter" is optional, it can contain a bool expression to show only specific result entries * "" can be specified in the syslog result and must contain an single parameter or pattern name. * "format" can contain a C like format string: e.g. "format=".2f"" * "title" is optional: alternative key title. * Unlike the result table, the unit attribute of a parameter or pattern is not taken into account. 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 tag Show tag description for the given input file, the given benchmark directory or a specific benchmark. If path to input file or benchmark directory is missing, current directory will be used. If no benchmark id is given, last benchmark found in directory will be used. 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" tags_tag Specify tag description and combination of tags that must be set. ... ... * "forced" is optional, if it is set to "true", you will be forced to describe every possible tag. (default: "false") * multiple "" and "" are allowed * In the "", you can write a description for the tag with the given name. 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=1717059854.0 JUBE-2.7.1/jube/info.py0000664000174700017470000006757514626040416016711 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 jube.util.util import jube.util.output import jube.conf import jube.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, jube.conf.CONFIGURATION_FILENAME) if os.path.isdir(dir_path) and os.path.exists(configuration_file): try: id_number = int(dir_name) parser = jube.jubeio.Parser(configuration_file) name_str, comment_str, tags = parser.benchmark_info_from_xml() tags_str = jube.conf.DEFAULT_SEPARATOR.join(tags) # Read timestamps from timestamps file timestamps = \ jube.util.util.read_timestamps( os.path.join(dir_path, jube.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 = (jube.util.output.text_boxed("Benchmarks found in \"{0}\":". format(path)) + "\n" + jube.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 = \ jube.util.output.text_boxed("{0} id:{1} tags:{2}\n\n{3}" .format(benchmark.name, benchmark.id, jube.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 = jube.util.util.read_timestamps( os.path.join(benchmark.bench_dir, jube.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, jube.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)) print(jube.util.output.text_line("=")) # Create parameter overview if benchmark.parametersets: print(jube.util.output.text_line("=")) print("\nParametersets info:") for parameterset_name, parameterset in benchmark.parametersets.items(): print("\n Parameterset name: " + parameterset_name) print(" Duplicate: " + parameterset.duplicate) parameter_info = [("name", "mode", "type", "seperator", "export", "unit", "update_mode", "duplicate", "value")] for parameter in parameterset.all_parameters: parameter_info.append((parameter.name, parameter.mode, parameter.parameter_type, parameter.separator, str(parameter.export), parameter.unit, parameter.update_mode, parameter.duplicate, parameter.value)) print(" Parameter:") print("\n" + jube.util.output.text_table(parameter_info, use_header_line=True, indent=2)) # Create substitute overview if benchmark.substitutesets: print(jube.util.output.text_line("=")) print("\nSubstitutesets info:") for substituteset_name, substituteset in benchmark.substitutesets.items(): print("\n Substituteset name: " + substituteset_name) file_info = [("in", "out", "out_mode")] for file in substituteset.files: file_info.append((file[1], file[0], file[2])) print(" IOFiles:") print("\n" + jube.util.output.text_table(file_info, use_header_line=True, indent=2)) sub_info = [("source", "dest", "mode")] for name, sub in substituteset.subs.items(): sub_info.append((sub.source, sub.dest, sub.mode)) print(" Subs:") print("\n" + jube.util.output.text_table(sub_info, use_header_line=True, indent=2)) # Create file overview if benchmark.filesets: print(jube.util.output.text_line("=")) print("\nFilesets info:") for fileset_name, fileset in benchmark.filesets.items(): print("\n Fileset name: " + fileset_name) file_info = [("type", "name", "path", "source_dir", "target_dir", "rel_path_ref", "active")] for file in fileset: name = file._name if file._name else "" rel_path_ref = "internal" if file._is_internal_ref else "external" file_info.append((file.file_type, name, file.path, file.source_dir, file.target_dir, rel_path_ref, str(file.active))) print(" Files:") print("\n" + jube.util.output.text_table(file_info, use_header_line=True, indent=2)) # Create pattern overview if benchmark.patternsets: print(jube.util.output.text_line("=")) print("\nPatternsets info:") for patternset_name, patternset in benchmark.patternsets.items(): print("\n Patternset name: " + patternset_name) pattern_info = [("name", "value", "default", "unit", "mode", "type", "dotall")] for pattern in patternset.pattern_storage: default = pattern.default_value if pattern.default_value else "" pattern_info.append((pattern.name, pattern.value, default, pattern.unit, pattern.mode, pattern.parameter_type, str(pattern.dotall))) for pattern in patternset.derived_pattern_storage: default = pattern.default_value if pattern.default_value else "" pattern_info.append((pattern.name, pattern.value, default, pattern.unit, pattern.mode, pattern.parameter_type, str(pattern.dotall))) print(" Pattern:") print("\n" + jube.util.output.text_table(pattern_info, use_header_line=True, indent=2)) # Create step overview if benchmark.steps: status_info = [("step_name", "#work", "#error", "#done", "last finished")] print(jube.util.output.text_line("=")) print("\nSteps info:") for step_name, workpackages in benchmark.workpackages.items(): print("\n Step name: " + step_name) step_info = [("depends", "work_dir", "suffix", "shared", "active", "export", "max_async", "iterations", "cycles", "procs", "do_log_file", )] step = benchmark.steps[step_name] # Get used sets and print out used_paramsets = step.get_used_sets(benchmark.parametersets) if used_paramsets: print(" Used Parametersets: " + ", ".join(used_paramsets)) used_patternsets = step.get_used_sets(benchmark.patternsets) if used_patternsets: print(" Used Patternsets: " + ", ".join(used_patternsets)) used_filesets = step.get_used_sets(benchmark.filesets) if used_filesets: print(" Used Filesets: " + ", ".join(used_filesets)) used_substitutesets = step.get_used_sets(benchmark.substitutesets) if used_substitutesets: print(" Used Substitutesets: " + ", ".join(used_substitutesets)) # Get attributes and print out depends = jube.conf.DEFAULT_SEPARATOR.join(step.depend) iterations = step.iterations work_dir = step.work_dir if step.work_dir else "" shared = step.shared_link_name if step.shared_link_name else "" do_log_file = step.do_log_file if step.do_log_file else "" step_info.append((depends, work_dir, step.suffix, shared, step.active, str(step.export), step.max_wps, str(iterations), str(step.cycles), str(step.procs), do_log_file)) print( "\n" + jube.util.output.text_table(step_info, use_header_line=True, indent=2)) # Get operation attributes and print out print(" Operations:") operation_info = [("do", "stdout", "stderr", "active", "done_file", "error_file", "break_file", "shared", "work_dir")] for operation in step.operations: stdout = operation.stdout_filename if operation.stdout_filename else "" stderr = operation.stderr_filename if operation.stderr_filename else "" async_file = operation.async_filename if operation.async_filename else "" error = operation.error_filename if operation.error_filename else "" break_file = operation.break_filename if operation.break_filename else "" work_dir = operation.work_dir if operation.work_dir else "" operation_info.append((operation.do, stdout, stderr, operation.active_string, async_file, error, break_file, str(operation.shared), work_dir)) print( "\n" + jube.util.output.text_table(operation_info, use_header_line=True, indent=2)) # Get status and print out cnt_done = 0 cnt_error = 0 last_finish = time.localtime(0) 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, jube.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 if iterations > 1: cnt = "{0}*{1}".format(len(workpackages) // iterations, iterations) else: cnt = str(len(workpackages)) status_info.append((step_name, cnt, str(cnt_error), str(cnt_done), last_finish_str)) # Create analyse overview if benchmark.analyser: print(jube.util.output.text_line("=")) print("\nAnalyser info:") for analyser_name, analyser in benchmark.analyser.items(): print("\n Analyser name: " + analyser_name) print(" Reduce: " + str(analyser.reduce)) print(" Used Patternsets: " + ", ".join(analyser.use)) for step_name, analyse in analyser.analyser.items(): print(" Analyse Files for Step " + step_name + ":") analyse_info = [("path", "use")] for file in analyse: analyse_info.append((file.path, ", ".join(file.use))) print("\n" + jube.util.output.text_table(analyse_info, use_header_line=True, indent=3)) # Create Result overview if benchmark.results: print(jube.util.output.text_line("=")) print("\nResult info:") for result_name, result in benchmark.results.items(): print("\n Result name: " + result_name) print(" Used Analyser: " + ", ".join(result.use)) result_type = result.result_type if result_type == "Table": table_info = [("name", "style", "sort", "seperator", "transpose", "filter")] column_info = [("column", "colw", "format", "title")] res_filter = result.res_filter if result.res_filter else "" table_info.append((result.name, result.style, ", ".join(result.sort), result.separator, str(result.transpose), res_filter)) print(" Table Info:") print("\n" + jube.util.output.text_table(table_info, use_header_line=True, indent=3)) for column in result._keys: colw = column.colw if column.colw else "" col_format = column.format if column.format else "" title = column.title if column.title else "" column_info.append((column.name, colw, col_format, title)) print(" Column Info:") print("\n" + jube.util.output.text_table(column_info, use_header_line=True, indent=3)) elif result_type == "Database": database_info = [("name", "primekeys", "file", "filter")] key_info = [("key", "title")] res_filter = result.res_filter if result.res_filter else "" database_info.append((result.name, ", ".join(result.primekeys), result.file, res_filter)) print(" Database Info:") print("\n" + jube.util.output.text_table(database_info, use_header_line=True, indent=3)) for key in result._keys: title = key.title if key.title else "" key_info.append((key.name, title)) print(" Key Info:") print("\n" + jube.util.output.text_table(key_info, use_header_line=True, indent=3)) elif result_type == "SysloggedResult": syslog_info = [("name", "address", "host", "port", "sort", "format", "filter")] key_info = [("key", "format", "title")] address = result.address if result.address else "" host = result.host if result.host else "" port = result.port if result.port else "" res_filter = result.res_filter if result.res_filter else "" syslog_info.append((result.name, address, host, port, ", ".join(result.sort), result.sys_format, res_filter)) print(" Syslog Info:") print("\n" + jube.util.output.text_table(syslog_info, use_header_line=True, indent=3)) for key in result._keys: key_format = key.format if key.format else "" title = key.title if key.title else "" key_info.append((key.name, key_format, title)) print(" Key Info:") print("\n" + jube.util.output.text_table(key_info, use_header_line=True, indent=3)) print(jube.util.output.text_line("=")) print(jube.util.output.text_line("=")) print("\nSteps status:") print("\n" + jube.util.output.text_table(status_info, use_header_line=True, indent=1)) if continue_possible: print("\n--- Benchmark not finished! ---\n") else: print("\n--- Benchmark finished ---\n") print(jube.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(jube.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 = jube.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(jube.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(jube.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(jube.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") def print_tag_documentation(dir, benchmark): """Print tag documentation concerning a specific input file""" infostr = \ jube.util.output.text_boxed("{0} \n\n{1}".format(benchmark.name, benchmark.comment)) print(infostr) print(" Path: {0}".format(os.path.abspath(dir))) tag_docu = [("tag name", "description")] for name, doku in benchmark.tag_docu.items(): tag_docu.append((name, doku)) print("\n" + jube.util.output.text_table(tag_docu, use_header_line=True, indent=1)) print(jube.util.output.text_line()) def print_benchmark_tag_documentation(benchmark): """Print tag documentation concerning a specific benchmark""" infostr = \ jube.util.output.text_boxed("{0} id:{1} tags:{2}\n\n{3}" .format(benchmark.name, benchmark.id, jube.conf.DEFAULT_SEPARATOR.join( benchmark.tags), benchmark.comment)) print(infostr) print(" Directory: {0}" .format(os.path.abspath(benchmark.bench_dir))) tag_docu = [("tag name", "description")] for name, doku in benchmark.tag_docu.items(): tag_docu.append((name, doku)) print("\n" + jube.util.output.text_table(tag_docu, use_header_line=True, indent=1)) print(jube.util.output.text_line()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/jubeio.py0000664000174700017470000023376114626040416017223 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 jube.util.util import Queue import jube.benchmark import jube.substitute import jube.parameter import jube.fileset import jube.pattern import jube.workpackage import jube.analyser import jube.step import jube.util.util import jube.util.output import jube.conf import jube.result_types.syslog import jube.result_types.table import jube.result_types.database import jube.util.yaml_converter import sys import re import copy import hashlib import jube.log from jube.util.version import StrictVersion LOGGER = jube.log.get_logger(__name__) class Parser(object): """JUBE XML input file parser""" def __init__(self, filename, tags=None, include_path=None, outpath=None, force=False, strict=False, command_name=None): 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._outpath = outpath self._force = force self._strict = strict self._command_name = command_name # run, continue, analyse, ... 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, check_tags=True): """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 = jube.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(jube.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, jube.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, jube.conf.JUBE_VERSION) try: inp = raw_input(info_str) except NameError: inp = input(info_str) if not inp.startswith("y"): return None, list(), list() # DEPRECATED: check_tags no longer allowed at global level, only in tags valid_tags = ["selection", "include-path", "parameterset", "benchmark", "tags", "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 < jube.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(jube.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(jube.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.") # Save the current tree before removing the invalid tags to preserve all available tags tag_tree = copy.deepcopy(tree) # Rerun removing invalid tags LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube.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 = jube.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"])) # DEPRECATED: check_tags no longer allowed at global level, only in tags # Read all global check_tags and check if necessary tags are given if check_tags: self._control_check_tags(tree.getroot()) # Read out tag documentation in tags-tag self._tag_docu = self._extract_tags(tag_tree.getroot(), check_tags) 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 _check_valid_tags(element, tags): """Check if element contains only valid tags""" return jube.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 = jube.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(jube.conf.DEFAULT_SEPARATOR)] for file_str in from_str.split(jube.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 += jube.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 = jube.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 jube.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( jube.conf.DEFAULT_SEPARATOR)])) benchmark_etree = jube.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 = jube.util.util.get_tree_elements(tree, "analyzer") analyser += jube.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 = jube.util.util.convert_type(pattern_name, 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] = \ jube.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 jube.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 = jube.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( jube.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( jube.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 = jube.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 = jube.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 = jube.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 _control_check_tags(self, tree): """ Check for given check_tags in the tree and check if required tags are set. """ check_tags = "" for element in tree.findall("check_tags"): check_tags += "(" + element.text + ")" if element != tree.findall("check_tags")[-1]: check_tags += " + " if check_tags != "": if not jube.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 ')\ .replace('^', ' xor '))) def _extract_tags(self, tree, check_tags=True): """ Extract tag documentation from tree and controll check_tags. Returns the tags with documentation found as dictionary. """ valid_tags = ["tag", "check_tags"] tags = dict() forced = False for tags_tree in tree.findall("tags"): # controll check tags if check_tags: self._control_check_tags(tags_tree) forced = tags_tree.get("forced", "false").strip().lower() == "true" or forced # find tag documentation for element in tags_tree: Parser._check_tag(element, valid_tags) if element.tag == "tag" and element.text is not None: tag_name = Parser._attribute_from_element(element, "name").strip() tag_docu = element.text.strip() if tag_docu != "": tags[tag_name] = tag_docu else: raise ValueError("The following tag description is empty: {0}" .format(tag_name)) elif element.tag == "tag" and element.text is None: tag_name = Parser._attribute_from_element(element, "name").strip() raise ValueError("The following tag description is empty: {0}" .format(tag_name)) # Get all available tags to check if tags are documented and have matching tag all_tags = list() for element in tree.findall(".//*[@tag]"): found_tag = re.findall(r"[\w'-]+", element.attrib["tag"]) all_tags.extend(found_tag) unused_tag_docu = list(set(tags.keys()) - set(all_tags)) if len(unused_tag_docu) and self._command_name == 'run': raise ValueError("Tag descriptions are only allowed for used tags. Tag: " "'{0}' isn't used in the input file." .format(", ".join(unused_tag_docu))) if forced: missing_tag_docu = list(set(all_tags) - set(tags.keys())) if len(missing_tag_docu): raise ValueError("The following tag description is required: " "{0}".format(", ".join(missing_tag_docu))) return tags 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() if self._outpath is None: 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)) # Change runtime outpath if specified else: outpath = self._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 = jube.benchmark.Benchmark(name, outpath, parametersets, substitutesets, filesets, patternsets, steps, analyser, results, results_order, comment, self._tags, self._tag_docu, 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 = jube.conf.DO_LOG_FILENAME if do_log_file == "True" else do_log_file do_log_file = jube.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(jube.conf.DEFAULT_SEPARATOR) if val.strip()) step = jube.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 = jube.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 = jube.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(jube.conf.DEFAULT_SEPARATOR)] else: use_names = list() for filename in file_etree.text.split( jube.conf.DEFAULT_SEPARATOR): file_obj = jube.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", jube.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( jube.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 = jube.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(jube.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 = jube.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() primekey = element.get("primekey") if primekey is not None: primekey = primekey.strip().lower() == "true" else: primekey = False database.add_key(key_name, format_string, title, primekey) 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( jube.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 = jube.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(jube.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 \ jube.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 = jube.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 = jube.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 = jube.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 = jube.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 = \ jube.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 = jube.fileset.Fileset(name) files = self._extract_files(elements[0]) for file_obj in files: if type(file_obj) is not jube.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 = jube.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 = jube.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=jube.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 jube.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 jube.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 = \ jube.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 = jube.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( jube.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(jube.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] = jube.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", jube.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 = jube.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(jube.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 = jube.fileset.Copy( path, name, is_internal_ref, active, source_dir, target_dir) elif etree_file.tag == "link": file_obj = jube.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 = jube.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] = \ jube.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] = jube.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( jube.util.output.element_tree_tostring( element, encoding="UTF-8"))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/log.py0000664000174700017470000001277314626040416016525 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 jube.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 = jube.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube.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 jube.conf.DEBUG_MODE: filename = jube.conf.LOGFILE_DEBUG_NAME mode = "default" filemode = jube.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("jube") _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(jube.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(jube.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 jube.conf.DEBUG_MODE: return setup_logging(filename=filename, mode="default") def only_console_log(): """Change to console log if not in debug mode.""" if jube.conf.DEBUG_MODE: return setup_logging(mode="console") def reset_logging(): """Reset logging to default.""" global LOGGING_MODE, LOGFILE_NAME LOGGING_MODE = jube.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube.conf.DEFAULT_LOGFILE_NAME ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/main.py0000664000174700017470000013110114626040416016653 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 jube.jubeio import jube.util.util import jube.util.output import jube.conf import jube.info import jube.help import jube.log import jube.completion import sys import os import re import shutil from jube.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 = jube.log.get_logger(__name__) def continue_benchmarks(args): """Continue benchmarks""" found_benchmarks = search_for_benchmarks(args) jube.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 jube.info.print_benchmark_status(benchmark) def tag(args): """Show tag documentation""" # Show tag docu out of inputfile if the given path is a file if os.path.isfile(args.dir): parser = jube.jubeio.Parser(args.dir) benchmarks = parser.benchmarks_from_xml(check_tags=False)[0] if benchmarks is None: return for benchmark in benchmarks.values(): jube.info.print_tag_documentation(args.dir, benchmark) # Show tag docu out of bench run if the given path is a directory 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: return jube.info.print_benchmark_tag_documentation(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 = jube.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 = jube.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 = jube.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(jube.help.HELP.keys()): print("{0}:".format(key)) print(jube.help.HELP[key]) else: if args.command in jube.help.HELP: if args.command in subparser: subparser[args.command].print_help() else: print(jube.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.") jube.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: jube.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: jube.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: jube.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(jube.conf.UPDATE_VERSION_URL) version = website.read().decode().strip() if StrictVersion(jube.conf.JUBE_VERSION) >= StrictVersion(version): LOGGER.info("Newest JUBE version {0} is already " "installed.".format(jube.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, jube.conf.JUBE_VERSION, jube.conf.UPDATE_URL)) except IOError as ioe: raise IOError("Cannot connect to {0}: {1}".format( jube.conf.UPDATE_VERSION_URL, str(ioe))) except ValueError as verr: raise ValueError("Cannot read version string from {0}: {1}".format( jube.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 = jube.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 = jube.log.matching_logs( args.command, available_logs) # Output the log file for log in matching: jube.log.log_print("BenchmarkID: {0} | Log: {1}".format( int(os.path.basename(benchmark_folder)), log)) jube.log.safe_output_logfile(log) # Inform user if any selected log was not found if not_matching: jube.log.log_print("Could not find logs: {0}".format( ",".join(not_matching))) def complete(args): """Handle shell completion""" jube.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.""" jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.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(jube.conf.JUBE_VERSION)) # Read existing benchmark configuration try: parser = jube.jubeio.Parser(os.path.join( benchmark_folder, jube.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 = jube.jubeio.Parser(os.path.join( benchmark_folder, jube.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, jube.conf.ANALYSE_FILENAME)): # Read existing analyse data parser = jube.jubeio.Parser(os.path.join( benchmark_folder, jube.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] jube.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 = jube.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 = jube.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, jube.conf.CONFIGURATION_FILENAME)): LOGGER.warning(("Configuration file \"{0}\" not found in " + "\"{1}\" or directory not readable.") .format(jube.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 = jube.util.util.get_current_id(args.dir) benchmark_folder = jube.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, jube.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""" jube.conf.HIDE_ANIMATIONS = args.hide_animation jube.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 jube.log.change_logfile_name( filename=os.path.join(os.path.dirname(path), jube.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(jube.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 # Get benchmark outpath out of environment if args.outpath not set if args.outpath is None: args.outpath = jube.util.util.check_and_get_benchmark_outpath() parser = jube.jubeio.Parser(path, tags, include_pathes, args.outpath, args.force, args.strict, args.subparser) 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 # Start benchmark run bench.new_run() # Run analyse if args.analyse or args.result: jube.log.change_logfile_name(os.path.join( bench.bench_dir, jube.conf.LOGFILE_ANALYSE_NAME)) bench.analyse() # Create result data if args.result: jube.log.change_logfile_name(os.path.join( bench.bench_dir, jube.conf.LOGFILE_RESULT_NAME)) bench.create_result(show=True) # Clean up when using debug mode if jube.conf.DEBUG_MODE: bench.delete_bench_dir() # Reset logging jube.log.only_console_log() def _continue_benchmark(benchmark_folder, args): """Continue existing benchmark""" jube.conf.EXIT_ON_ERROR = args.error benchmark = _load_existing_benchmark(args, benchmark_folder) if benchmark is None: return # Change logfile jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.conf.LOGFILE_CONTINUE_NAME)) # Run existing benchmark benchmark.run() # Run analyse if args.analyse or args.result: jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse() # Create result data if args.result: jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.conf.LOGFILE_RESULT_NAME)) benchmark.create_result(show=True) # Clean up when using debug mode if jube.conf.DEBUG_MODE: benchmark.reset_all_workpackages() # Reset logging jube.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 jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.conf.LOGFILE_ANALYSE_NAME)) LOGGER.info(jube.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, jube.conf.ANALYSE_FILENAME)): LOGGER.info(">>> Analyse data storage: {0}".format(os.path.join( benchmark_folder, jube.conf.ANALYSE_FILENAME))) else: LOGGER.info(">>> Analyse data storage \"{0}\" not created!".format( os.path.join(benchmark_folder, jube.conf.ANALYSE_FILENAME))) LOGGER.info(jube.util.output.text_line()) # Reset logging jube.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: jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse(show_info=False) # Change logfile jube.log.change_logfile_name(os.path.join( benchmark_folder, jube.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 jube.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 = jube.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, jube.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, jube.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( jube.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 (relative to the " "execution location)"} } } # 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": "+"} } } # tag subparser subparser_configuration["tag"] = { "help": "show tag documentation", "func": tag, "arguments": { ('dir',): {"metavar": "PATH", "nargs": "?", "help": "path to input file or 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=jube.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(jube.help.HELP) + ["ALL"]) max_word_length = max(map(len, help_keys)) + 4 # calculate max number of keyword columns max_columns = jube.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 = jube.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.""" jube.help.load_help() parser = _get_args_parser()[0] if command is None: args = parser.parse_args() else: args = parser.parse_args(command) jube.conf.DEBUG_MODE = args.debug # Get verbose level out from args or env if args.verbose > 0: jube.conf.VERBOSE_LEVEL = args.verbose # Print warning if verbose level is out of range if jube.conf.VERBOSE_LEVEL < 0 or jube.conf.VERBOSE_LEVEL > 3: print("The verbosity level is out of range. It must be -v, -vv or -vvv but the " "current level is -{0}.".format("v"*jube.conf.VERBOSE_LEVEL)) exit(1) else: jube.conf.VERBOSE_LEVEL = jube.util.util.check_and_get_verbose_level() if jube.conf.VERBOSE_LEVEL < 0 or jube.conf.VERBOSE_LEVEL > 3: print("The verbosity level is out of range. Supported values are 0, 1, 2 or 3. " "The current level is {0}.".format(jube.conf.VERBOSE_LEVEL)) exit(1) if jube.conf.VERBOSE_LEVEL > 0: args.hide_animation = True # Set new umask if JUBE_GROUP_NAME is used current_mask = os.umask(0) if (jube.util.util.check_and_get_group_id() is not None) and \ (current_mask > 2): current_mask = 2 os.umask(current_mask) if args.subparser: jube.log.setup_logging(mode="console", verbose=(jube.conf.VERBOSE_LEVEL == 1) or (jube.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)) jube.log.reset_logging() exit(1) else: parser.print_usage() jube.log.reset_logging() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/parameter.py0000664000174700017470000011053714626040416017721 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 jube.util.util import jube.conf import jube.log import re import inspect LOGGER = jube.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(jube.util.util.ensure_list(self._parameters[parameter.name]._value)+jube.util.util.ensure_list(parameter._value))) value.sort() return jube.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 jube.conf.ALLOWED_SCRIPTTYPES.union( jube.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 < jube.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 jube.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 jube.parameter import xml.etree.ElementTree as ET LOGGER = jube.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 = jube.parameter.Parameterset("pattern") self._derived_pattern = jube.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(jube.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 jube.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 jube.conf.ALLOWED_SCRIPTTYPES and force_evaluation and self._default is not None): final_sub = True force_evaluation = False param, changed = \ jube.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=1717059854.0 JUBE-2.7.1/jube/result.py0000664000174700017470000002330014626040416017246 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 jube.util.util import xml.etree.ElementTree as ET import re import jube.log LOGGER = jube.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 result_type(self): """Return class name""" return type(self).__name__ @property def use(self): """Return the result name""" return self._use @property def res_filter(self): """Return the result filter""" return self._res_filter @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 = \ jube.util.util.convert_type(par.name, 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 = jube.util.util.substitution( self._res_filter, analyse_dict) if not jube.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=1717059854.3811166 JUBE-2.7.1/jube/result_types/0000775000174700017470000000000014626040416020122 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/result_types/__init__.py0000664000174700017470000000145114626040416022234 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 . """jube.result_types package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/result_types/database.py0000664000174700017470000002242314626040416022243 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 jube.result_types.keyvaluesresult import KeyValuesResult from jube.result import Result import xml.etree.ElementTree as ET import jube.log LOGGER = jube.log.get_logger(__name__) class Database(KeyValuesResult): """A database result""" class DatabaseData(KeyValuesResult.KeyValuesData): """Database data""" def __init__(self, name_or_other, primekeys, db_file): 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._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.resulting_name for k in self.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 = {keys[i]: type(self.data[0][i]).__name__.replace( 'str', 'text') for i in range(len(self.keys))} db_col_insert_types = str(key_dtypes).replace( '{', '(').replace('}', ')').replace("'", '').replace(':', '') # Add key with primekey=true to primekeys and use set to remove duplicates self._primekeys = list(set(self._primekeys + [k.resulting_name for k in self._keys if k.primekey])) if len(self._primekeys) > 0: db_col_insert_types = db_col_insert_types[:-1] + \ ", PRIMARY KEY ({}))".format(', '.join(map(repr, self._primekeys))) LOGGER.warning("The `primekeys` attribute of the ``-tag is deprecated. " "Instead, use the new `primekey` attribute of the ``-tag. " "(..)") # 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 self.data]) 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)) class Column(KeyValuesResult.DataKey): """Class represents one database column""" def __init__(self, name, title=None, format_string=None, primekey=False): KeyValuesResult.DataKey.__init__(self, name, title, format_string, None) self._primekey = primekey @property def primekey(self): """Column width""" return self._primekey def etree_repr(self): """Return etree object representation""" column_etree = KeyValuesResult.DataKey.etree_repr(self) if self._primekey: column_etree.attrib["primekey"] = str(self._primekey).lower() return column_etree def __init__(self, name, res_filter=None, primekeys=None, db_file=None): KeyValuesResult.__init__(self, name, None, res_filter) self._primekeys = primekeys self._db_file = db_file @property def file(self): """Return the database file""" return self._db_file @property def primekeys(self): """Return the database primekeys""" return self._primekeys def add_key(self, name, format_string=None, title=None, primekey=False): """Add an additional key to the dataset""" self._keys.append(Database.Column(name, title, format_string, primekey)) def create_result_data(self, style=None, select=None, exclude=None): """Create result data""" result_data = KeyValuesResult.create_result_data(self, select, exclude, preserve_datatype=True) 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=1717059854.0 JUBE-2.7.1/jube/result_types/genericresult.py0000664000174700017470000002217714626040416023360 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 jube.result import Result import jube.log import xml.etree.ElementTree as ET import operator import jube.util.util import jube.util.output LOGGER = jube.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=1717059854.0 JUBE-2.7.1/jube/result_types/keyvaluesresult.py0000664000174700017470000003121214626040416023742 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 jube.result import Result import jube.log import xml.etree.ElementTree as ET import operator import jube.util.util import jube.util.output LOGGER = jube.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 @property def sort(self): """Return the result style""" return self._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, preserve_datatype=False): """Create the result data. The keys in the select list are selected for these results, while the keys in the exclude list are excluded from the results. The preserve_datatype parameter specifies whether the data type of the value should be preserved or converted to a string.""" 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: [jube.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 = jube.util.output.format_value( key.format, dataset[key.name]) elif preserve_datatype: value = 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=1717059854.0 JUBE-2.7.1/jube/result_types/syslog.py0000664000174700017470000001446514626040416022026 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 jube.result_types.keyvaluesresult import KeyValuesResult from jube.result import Result import xml.etree.ElementTree as ET import jube.log import jube.conf import logging.handlers LOGGER = jube.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 jube.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 = jube.conf.SYSLOG_FMT_STRING else: self._syslog_fmt_string = syslog_fmt_string @property def address(self): """Return syslog address""" return self._syslog_address @property def host(self): """Return syslog host""" return self._syslog_host @property def port(self): """Return syslog port""" return self._syslog_port @property def sys_format(self): """Return syslog format""" return self._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"] = \ jube.conf.DEFAULT_SEPARATOR.join(self._sort_names) for key in self._keys: syslog_etree.append(key.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/result_types/table.py0000664000174700017470000001647214626040416021575 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 jube.result_types.keyvaluesresult import KeyValuesResult from jube.result import Result import xml.etree.ElementTree as ET import jube.log import jube.util.output LOGGER = jube.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 = jube.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 += jube.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=jube.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 @property def style(self): """Return the result style""" return self._style @property def separator(self): """Return the result separator""" return self._separator @property def transpose(self): """Return the result transpose""" return self._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"] = \ jube.conf.DEFAULT_SEPARATOR.join(self._sort_names) for column in self._keys: table_etree.append(column.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/step.py0000664000174700017470000010333714626040416016714 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 jube.util.util import jube.conf import jube.log import jube.parameter LOGGER = jube.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"] = \ jube.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 = jube.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 operations(self): """Return step dos""" return self._operations @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 @property def work_dir(self): """Return alternative working directory""" return self._alt_work_dir @property def suffix(self): """Return alternative working directory""" return self._suffix 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 = jube.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 = jube.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 = jube.parameter.Parameterset() # step name parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_step_name", self._name, update_mode=jube.parameter.JUBE_MODE)) # iterations parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_step_iterations", str(self._iterations), parameter_type="int", update_mode=jube.parameter.JUBE_MODE)) # cycles parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_step_cycles", str(self._cycles), parameter_type="int", update_mode=jube.parameter.JUBE_MODE)) # default worpackage cycle, will be overwritten by specific worpackage # cycle parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_cycle", "0", parameter_type="int", update_mode=jube.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 = jube.parameter.Parameterset() if local_parameterset is None: local_parameterset = jube.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( jube.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=jube.parameter.USE_MODE): update_parameters.add_parameterset( local_parameterset.get_updatable_parameter( jube.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=jube.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 = jube.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: jube.util.util.convert_type(parameter.name, parameter.parameter_type, parameter.value) # --- Enable workpackage dir cache --- workpackage.allow_workpackage_dir_caching() if workpackage.active: created_workpackages.append(workpackage) else: jube.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 do(self): """Get do""" return self._do @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 break_filename(self): """Get break filename""" return self._break_filename @property def async_filename(self): """Get async filename""" return self._async_filename @property def shared(self): """Shared operation?""" return self._shared @property def active_string(self): """Get active""" return self._active @property def work_dir(self): """Get work directory""" return self._work_dir def active(self, parameter_dict): """Return active status of the current operation depending on the given parameter_dict""" active_str = jube.util.util.substitution(self._active, parameter_dict) return jube.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 = jube.util.util.substitution(self._do, parameter_dict) # Remove leading and trailing ; because otherwise ;; will cause # trouble when adding ; env do = do.strip(";") if (not jube.conf.DEBUG_MODE) and (do.strip() != ""): # Change stdout if self._stdout_filename is not None: stdout_filename = jube.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 = jube.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 = jube.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(jube.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 jube.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 = jube.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube.conf.ENVIRONMENT_INFO abs_info_file_path = \ os.path.abspath(os.path.join(work_dir, env_file_name)) # Select unix shell shell = jube.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 jube.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 jube.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 jube.conf.VERBOSE_LEVEL > 1: while True: read_out = sub.stdout.read( jube.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(jube.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) > jube.conf.ERROR_MSG_LINES else "", "\n".join(stderr_msg[ -jube.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 = jube.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 = jube.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 jube.conf.DEBUG_MODE: LOGGER.debug(" skip waiting") else: continue_op = False # Search for error file if self._error_filename is not None: error_filename = jube.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 jube.conf.DEBUG_MODE: LOGGER.debug(" skip error") else: do = jube.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 = jube.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube.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 = jube.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(jube.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=1717059854.0 JUBE-2.7.1/jube/substitute.py0000664000174700017470000001464414626040416020156 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 jube.util.util import jube.util.output import jube.conf import xml.etree.ElementTree as ET import jube.log import shutil import codecs LOGGER = jube.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 @property def files(self): """Return iofiles""" return self._files @property def subs(self): """Return subs""" return self._substitute_dict 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 = jube.util.util.substitution(sub.source, parameter_dict) new_dest = jube.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 = jube.util.util.substitution(infile_name, parameter_dict) outfile = jube.util.util.substitution(outfile_name, parameter_dict) LOGGER.debug(" substitute {0} -> {1}".format(infile, outfile)) LOGGER.debug(" substitute:\n" + jube.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 jube.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=1717059854.3811166 JUBE-2.7.1/jube/util/0000775000174700017470000000000014626040416016335 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/util/__init__.py0000664000174700017470000000144114626040416020446 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 . """jube.util package""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/jube/util/output.py0000664000174700017470000001715714626040416020262 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 jube.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 = "#" * jube.conf.DEFAULT_WIDTH for line in text.split("\n"): box += "\n" lines = ["# {0}".format(element) for element in textwrap.wrap(line.strip(), jube.conf.DEFAULT_WIDTH - 2)] if len(lines) == 0: box += "#" else: box += "\n".join(lines) box += "\n" + "#" * jube.conf.DEFAULT_WIDTH return box def text_line(char="#"): """Return a horizonal ASCII line""" return char * jube.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], jube.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, jube.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 = jube.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=1717059854.0 JUBE-2.7.1/jube/util/util.py0000664000174700017470000004325014626040416017670 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 jube.log import time import jube.conf import grp import pwd LOGGER = jube.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: raise ValueError("'{0}' cannot be parsed because the" "comma is no longer supported in " "'check_tags' and 'tag' attributes. " "Use '|', '+' and '!' instead." .format(tag_string)) 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=jube.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 < jube.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(name, 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}\" from \"{name}\" cannot be represented as a \"{value_type}\"") else: result_value = value if value_type_incorrect: LOGGER.debug(f"Warning: \"{value}\" from \"{name}\" 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 = jube.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 check_and_get_verbose_level(): """Read environment var JUBE_VERBOSE and return verbose level""" verbose_level = 0 if "JUBE_VERBOSE" in os.environ: try: verbose_level = int(os.environ["JUBE_VERBOSE"]) except ValueError: print("Failed to parse JUBE_VERBOSE variable '{}'. " "Accepted values are numbers from 0 to 3." .format(os.environ["JUBE_VERBOSE"])) exit(1) return verbose_level def check_and_get_benchmark_outpath(): """Read environment var JUBE_BENCHMARK_OUTPATH and return outpath""" env_outpath = None if "JUBE_BENCHMARK_OUTPATH" in os.environ: env_outpath = os.environ["JUBE_BENCHMARK_OUTPATH"] return env_outpath 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=1717059854.0 JUBE-2.7.1/jube/util/version.py0000664000174700017470000001502214626040416020374 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=1717059854.0 JUBE-2.7.1/jube/util/yaml_converter.py0000664000174700017470000003150014626040416021737 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 jube.log import jube.conf import jube.util.output import os import copy import jube.util.util try: from StringIO import StringIO as IOStream except ImportError: from io import BytesIO as IOStream LOGGER = jube.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", "tags"], "/benchmark": ["benchmark", "parameterset", "fileset", "substituteset", "patternset", "selection", "include-path", "check_tags", "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"], "tags": ["check_tags", "tag"], "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 < jube.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 = jube.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(jube.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 jube.util.util.valid_tags(path["tag"], self._tags): continue 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 if type(val) is list: for element in val: YAML_Converter.create_tag(key, element, new_node) else: 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=1717059854.0 JUBE-2.7.1/jube/workpackage.py0000664000174700017470000011272114626040416020234 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 jube.util.util import jube.util.output import jube.conf import jube.log import jube.parameter import jube.step import os import re import stat import shutil LOGGER = jube.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 = jube.util.util.substitution(active, parameter) # Evaluate active state return jube.util.util.eval_bool(active) @property def done(self): """Workpackage done?""" done_file = os.path.join(self.workpackage_dir, jube.conf.WORKPACKAGE_DONE_FILENAME) exist = os.path.exists(done_file) if jube.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, jube.conf.WORKPACKAGE_DONE_FILENAME) if jube.conf.DEBUG_MODE: done_file = done_file + "_DEBUG" if set_done: fout = open(done_file, "w") fout.write(jube.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, jube.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, jube.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 = jube.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( jube.conf.WORKPACKAGE_DONE_FILENAME, operation_number)) if set_done is None: exist = os.path.exists(done_file) if jube.conf.DEBUG_MODE: exist = exist or os.path.exists(done_file + "_DEBUG") return exist else: if jube.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))): jube.util.util.update_timestamps( os.path.join(self._benchmark.bench_dir, jube.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 = jube.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 = jube.parameter.Parameterset() parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_status", self.status(), parameter_type="string", update_mode=jube.parameter.JUBE_MODE)) self.parameterset.update_parameterset(parameterset) def get_jube_cycle_parameterset(self): """Return parameterset which contains cycle related information""" parameterset = jube.parameter.Parameterset() # worpackage cycle parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_cycle", str(self._cycle), parameter_type="int", update_mode=jube.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 = jube.parameter.Parameterset() # workpackage id parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_id", str(self._id), parameter_type="int", update_mode=jube.parameter.JUBE_MODE)) # workpackage id with padding parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_padid", jube.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube.parameter.JUBE_MODE)) # workpackage status parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_status", self.status(), parameter_type="string", update_mode=jube.parameter.JUBE_MODE)) # workpackage iteration parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_iteration", str(self._iteration), parameter_type="int", update_mode=jube.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( jube.parameter.Parameter. create_parameter("jube_wp_relpath", path, update_mode=jube.parameter.JUBE_MODE, eval_helper=self.create_relpath)) # workpackage absolute folder path parameterset.add_parameter( jube.parameter.Parameter. create_parameter("jube_wp_abspath", path, update_mode=jube.parameter.JUBE_MODE, eval_helper=self.create_abspath)) # parent workpackage id for parent in self._parents: parameterset.add_parameter( jube.parameter.Parameter. create_parameter(("jube_wp_parent_{0}_id") .format(parent.step.name), str(parent.id), parameter_type="int", update_mode=jube.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 = jube.parameter.Parameter.create_parameter( "jube_wp_envstr", env_str, no_templates=True, update_mode=jube.parameter.JUBE_MODE, eval_helper=jube.parameter.StaticParameter.fix_export_string) parameterset.add_parameter(env_par) # environment export list parameterset.add_parameter( jube.parameter.Parameter.create_parameter( "jube_wp_envlist", ",".join([name for name in parameter_names]), no_templates=True, update_mode=jube.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 = \ jube.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 = jube.util.util.substitution(suffix, parameter) suffix = "_" + os.path.expandvars(os.path.expanduser(suffix)) path = "{path}_{step_name}{suffix}".format( path=jube.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 = jube.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 = jube.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 = jube.log.LOGFILE_NAME.split('/')[-1] jube.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=jube.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 += jube.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(jube.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 jube.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 = jube.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 += jube.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 jube.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=1717059854.3731167 JUBE-2.7.1/platform/0000775000174700017470000000000014626040416016257 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717059854.3811166 JUBE-2.7.1/platform/lsf/0000775000174700017470000000000014626040416017043 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/platform/lsf/platform.xml0000775000174700017470000000701614626040416021420 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=1717059854.0 JUBE-2.7.1/platform/lsf/submit.job.in0000775000174700017470000000120214626040416021445 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=1717059854.3811166 JUBE-2.7.1/platform/moab/0000775000174700017470000000000014626040416017175 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/platform/moab/chainJobs.sh0000775000174700017470000000052314626040416021434 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=1717059854.0 JUBE-2.7.1/platform/moab/platform.xml0000664000174700017470000000737114626040416021553 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=1717059854.0 JUBE-2.7.1/platform/moab/submit.job.in0000664000174700017470000000123514626040416021602 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=1717059854.3811166 JUBE-2.7.1/platform/pbs/0000775000174700017470000000000014626040416017043 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/platform/pbs/chainJobs.sh0000775000174700017470000000052314626040416021302 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=1717059854.0 JUBE-2.7.1/platform/pbs/platform.xml0000664000174700017470000000717614626040416021424 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=1717059854.0 JUBE-2.7.1/platform/pbs/submit.job.in0000664000174700017470000000122414626040416021446 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=1717059854.3851166 JUBE-2.7.1/platform/slurm/0000775000174700017470000000000014626040416017421 5ustar00gitlab-runnergitlab-runner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/platform/slurm/chainJobs.sh0000775000174700017470000000113214626040416021655 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=1717059854.0 JUBE-2.7.1/platform/slurm/platform.xml0000664000174700017470000001027714626040416021776 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=1717059854.0 JUBE-2.7.1/platform/slurm/submit.job.in0000664000174700017470000000143014626040416022023 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=1717059854.3851166 JUBE-2.7.1/setup.cfg0000664000174700017470000000004614626040416016254 0ustar00gitlab-runnergitlab-runner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717059854.0 JUBE-2.7.1/setup.py0000664000174700017470000001253214626040416016150 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.7.1', 'packages': ['jube','jube.result_types','jube.util'], 'package_data': {'jube': ['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.")