././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.919361 JUBE-2.5.1/0000755000175000017500000000000000000000000013447 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/AUTHORS0000644000175000017500000000103500000000000014516 0ustar00sierrousierrou00000000000000# 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 Sebastian Lührs Thomas Breuer Kay Thust Julia Wellmann Alexander Trautmann Alexandre Strube Andreas Klasen Andreas Herten Filipe Guimarães Sebastian Achilles Jan-Oliver Mirus Wolfgang Frings Universiteit Gent Andy Georges ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7775068 JUBE-2.5.1/JUBE.egg-info/0000755000175000017500000000000000000000000015666 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329994.0 JUBE-2.5.1/JUBE.egg-info/PKG-INFO0000644000175000017500000000362300000000000016767 0ustar00sierrousierrou00000000000000Metadata-Version: 1.1 Name: JUBE Version: 2.5.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 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. 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. 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329994.0 JUBE-2.5.1/JUBE.egg-info/SOURCES.txt0000644000175000017500000000637400000000000017564 0ustar00sierrousierrou00000000000000AUTHORS 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.rnc contrib/schema/jube.xsd docs/JUBE.pdf examples/cycle/cycle.xml examples/cycle/cycle.yaml examples/dependencies/dependencies.xml examples/dependencies/dependencies.yaml examples/do_log/do_log.xml examples/do_log/do_log.yaml examples/do_log/loreipsum1 examples/do_log/loreipsum2 examples/do_log/loreipsum3 examples/do_log/loreipsum4 examples/do_log/loreipsum5 examples/duplicate/duplicate.xml examples/duplicate/duplicate.yaml examples/environment/environment.xml examples/environment/environment.yaml examples/files_and_sub/file.in examples/files_and_sub/files_and_sub.xml examples/files_and_sub/files_and_sub.yaml examples/hello_world/hello_world.xml examples/hello_world/hello_world.yaml examples/include/include_data.xml examples/include/include_data.yaml examples/include/main.xml examples/include/main.yaml examples/iterations/iterations.xml examples/iterations/iterations.yaml examples/jobsystem/job.run.in examples/jobsystem/jobsystem.xml examples/jobsystem/jobsystem.yaml examples/parallel_workpackages/parallel_workpackages.xml examples/parallel_workpackages/parallel_workpackages.yaml examples/parameter_dependencies/include_file.xml examples/parameter_dependencies/include_file.yaml examples/parameter_dependencies/parameter_dependencies.xml examples/parameter_dependencies/parameter_dependencies.yaml examples/parameter_update/parameter_update.xml examples/parameter_update/parameter_update.yaml examples/parameterspace/parameterspace.xml examples/parameterspace/parameterspace.yaml examples/result_creation/result_creation.xml examples/result_creation/result_creation.yaml examples/result_database/result_database.xml examples/result_database/result_database.yaml examples/result_database/result_database_filter.xml examples/scripting_parameter/scripting_parameter.xml examples/scripting_parameter/scripting_parameter.yaml examples/scripting_pattern/scripting_pattern.xml examples/scripting_pattern/scripting_pattern.yaml examples/shared/shared.xml examples/shared/shared.yaml examples/statistic/statistic.xml examples/statistic/statistic.yaml examples/tagging/tagging.xml examples/tagging/tagging.yaml examples/yaml/hello_world.yaml examples/yaml/special_values.yaml jube2/__init__.py jube2/analyser.py jube2/benchmark.py jube2/completion.py jube2/conf.py jube2/fileset.py jube2/help.py jube2/help.txt jube2/info.py jube2/jubeio.py jube2/log.py jube2/main.py jube2/parameter.py jube2/pattern.py jube2/result.py jube2/step.py jube2/substitute.py jube2/workpackage.py jube2/result_types/__init__.py jube2/result_types/database.py jube2/result_types/genericresult.py jube2/result_types/keyvaluesresult.py jube2/result_types/syslog.py jube2/result_types/table.py jube2/util/__init__.py jube2/util/output.py jube2/util/util.py jube2/util/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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329994.0 JUBE-2.5.1/JUBE.egg-info/dependency_links.txt0000644000175000017500000000000100000000000021734 0ustar00sierrousierrou00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329994.0 JUBE-2.5.1/JUBE.egg-info/requires.txt0000644000175000017500000000000700000000000020263 0ustar00sierrousierrou00000000000000pyyaml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329994.0 JUBE-2.5.1/JUBE.egg-info/top_level.txt0000644000175000017500000000000600000000000020414 0ustar00sierrousierrou00000000000000jube2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/LICENSE0000644000175000017500000010451300000000000014460 0ustar00sierrousierrou00000000000000 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 . ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.918362 JUBE-2.5.1/PKG-INFO0000644000175000017500000000362300000000000014550 0ustar00sierrousierrou00000000000000Metadata-Version: 1.1 Name: JUBE Version: 2.5.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 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. 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. 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/README.md0000644000175000017500000000571400000000000014735 0ustar00sierrousierrou00000000000000JUBE Benchmarking Environment Copyright (C) 2008-2022 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 http://www.gnu.org/licenses/. ---- # Prerequisites JUBE version 2 is written in the Python programming language. You need Python 3.2 (or a higher version) to run the program. JUBE is not compatible to Python2.x any longer. # Installation After download, unpack the distribution file `JUBE-.tar.gz` with: ```bash tar -xf JUBE-.tar.gz ``` You can install the files to your `$HOME/.local` directory by using: ```bash cd JUBE- python setup.py install --user ``` `$HOME/.local/bin` must be inside your `$PATH` environment variable to use JUBE in an easy way. Instead you can also specify a self defined path prefix: ```bash python setup.py install --prefix= ``` You might be asked during the installation to add your path (and some subfolders) to the `$PYTHONPATH` environment variable (this should be stored in your profile settings): ```bash export PYTHONPATH=:$PYTHONPATH ``` Another option is to use `pip[3]` for installation (including download): ```bash pip3 install http://apps.fz-juelich.de/jsc/jube/jube2/download.php?version=latest --user # or pip3 install http://apps.fz-juelich.de/jsc/jube/jube2/download.php?version=latest --prefix= ``` In addition it is useful to also set the `$PATH` variable again. To check the installation you can run: ``` jube --version ``` Without the `--user` or `--prefix` argument, JUBE will be installed in the standard system path for Python packages. # Acknowledgments We gratefully acknowledge the following reserach projects and institutions for their support in the development of JUBE2 and granting compute time to develop JUBE2. - 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) Furthermore, we gratefully acknowledge all the people and institutions having contributed to this project with their expertise, time and passion in any way. A subset of these people and institutions can be found within the AUTHORS file. # Further Information For further information please see the documentation: http://www.fz-juelich.de/jsc/jube Contact: [jube.jsc@fz-juelich.de](mailto:jube.jsc@fz-juelich.de) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/RELEASE_NOTES0000644000175000017500000005103400000000000015425 0ustar00sierrousierrou00000000000000Release notes ************* 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7795258 JUBE-2.5.1/bin/0000755000175000017500000000000000000000000014217 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/bin/jube0000755000175000017500000000200400000000000015066 0ustar00sierrousierrou00000000000000#!/usr/bin/env python3 # JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Executable script for main program""" from __future__ import (print_function, unicode_literals, division) import jube2.main if __name__ == "__main__": jube2.main.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/bin/jube-autorun0000755000175000017500000000624000000000000016567 0ustar00sierrousierrou00000000000000#!/usr/bin/env bash # JUBE Benchmarking Environment # Copyright (C) 2008-2022 # 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7335143 JUBE-2.5.1/contrib/0000755000175000017500000000000000000000000015107 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7834988 JUBE-2.5.1/contrib/schema/0000755000175000017500000000000000000000000016347 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/contrib/schema/jube.dtd0000644000175000017500000001666500000000000020007 0ustar00sierrousierrou00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/contrib/schema/jube.rnc0000644000175000017500000001625300000000000020007 0ustar00sierrousierrou00000000000000# 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)*, (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* } } attlist.include-path &= attribute tag { text }? path = element path { attlist.path, text } attlist.path &= attribute tag { 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 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 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 | 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 }? syslog = element syslog { attlist.syslog, (key | \include)* } attlist.syslog &= attribute name { text }, attribute address { text }?, attribute host { text }?, attribute port { text }?, attribute format { text }?, attribute sort { text }?, attribute filter { text }?, attribute tag { text }? column = element column { attlist.column, text } attlist.column &= attribute colw { text }?, attribute format { text }?, attribute title { text }?, attribute tag { text }? key = element key { attlist.key, text } attlist.key &= attribute format { text }?, attribute title { text }?, attribute tag { text }? file = element file { attlist.file, text } attlist.file &= attribute tag { text }?, attribute use { text }? \include = element include { attlist.include, empty } attlist.include &= attribute from { text }, attribute path { text }?, attribute tag { text }? start = jube any = (element * { attribute * { text }*, any } | text)* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/contrib/schema/jube.xsd0000644000175000017500000004052200000000000020017 0ustar00sierrousierrou00000000000000 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7844982 JUBE-2.5.1/docs/0000755000175000017500000000000000000000000014377 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/docs/JUBE.pdf0000644000175000017500000130162300000000000015625 0ustar00sierrousierrou00000000000000%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 1425 /Filter /FlateDecode >> stream xXM6W\ 6RH E6}R=%q͛HZHᅑJ*28( mPNe X'TV֙6Jh/g^2$$Q1DA"Y ,̫hBi8 Q8 4k\8:-[U`f .x؋ +#TB*H\8iX  #BE*# >{qOF%'UiN%EH&l}{ثSU)gxVF8exM BB,tjh2N==Ps}I~nY'{[o~ԇ<]r:EHCeg:&!\g ٙuorx,Uy)_SWu 8mFS|ؒm ܝ&'Sʛ}]_ڿ#?_"nd:Te$c>IAMa'ꗷW3[Ŕϻ3(lѼ³`KTɞ4喺ºTwb1d4]̭E!on:?#GJ_)|pBv Wky},=PKƁ9_L)Bti쌤vR$uYf$\Eu^uj=ʴ#^s׼bRv[xf|w HSm>x2r_[]3tZgƄqv{FQ?eˈ=[-U)akVH퇂5_=D]jT,6=,[Y3%[+}iG= ]EfIHf-,zo{fl3]:|SJ>!;.'_uYJ\Oh>dȗ"][V`-݇e,Mϴ@'uO._4 endstream endobj 228 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 229 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 231 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 233 0 obj << /Length 320 /Filter /FlateDecode >> stream xڵQNP+f&0ܹ.%Ŭ>6#6+rqNM{<)CѼzٗݡD{XPI&7i~6E["e endstream endobj 226 0 obj << /Type /XObject /Subtype /Image /Width 200 /Height 89 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 238 0 R /Length 11613 /Filter /FlateDecode >> stream xx\W8N@ ², c,r-uFg$z{ޥQMΗܹ{~q;nl;;;xjecbqd{=kgjQ*ޖER<8ͭs#~A+.}88z뮡%-,{6q%]#k<=umtqR+ϝ&>g@|ΐ!3^@̩I֛8ay_| 6yǓT4HX/qE*DVҊ@e632ZŲ; ~/zDI*7/kB꧉o]vn<#ۆWMzD)F_>P^@- fǓx#;@U+*;טEb,ecS |TW; UDY.;cS!8,靫.ϐʤǶш'ܿYYQ*VHmE_9OSfHz9= }|K=c *r/ 8^[2N9Q] =Ә\.hkPuE#%^\#szRA.]-{L}3lZz5U_׽ Cړ&GbA!lK;?}S :L.5 bT5"9ydO=kR^2qhG_:GEIOu$?LJ=7#!d/:h8N:^ қMH=Cܻ+Bϟrec1y7N.o? O.pa|~mqi{> kf "Ce@{ %ҒZ[0 >dsH߰g%P/n!\c M7;=MNkX'ۖǕka;\1T4 CE3TLDy7\ @\ \ >\ AU<6?CAUP.np/_H`@gHI O$^wvq̚fXHt~T\ks5;OL^AT ;^ \,}C1$y7>\P9aPUujCeHc_hf?G`a@,sg ԏ-mY * * իRP̚C $6:+KCJZjgݐ wɕD}+,Wm7LqB *һטX"lZ o]q!'Wlck ֐!w פg+U+5k^+TU\U;Pe> 7"QKR`%]#`7UO>]o:$.?\pz;"Ϸٻ + T^])uԀt,NW6\o[&{uNn^sBEOEҁKBx_hS[Fvk&k[MUt{B4gΠTUu}Z"}J' ? nwbN-Հ;}btEȩ>{.AŒ-xK Z 3[ F~n.^?]zQhԒR*V*0[\]~z"I? *hTrZT/艋"U~!%-`tͭnJ߇yAv\ U[r5Q_PQuByơ9  M1͐왕͕-]l1h6S2 J.GP^OV'D]G_,J^Ob mn\aEڅXoLn>5vTU&T :XR;0hǦ:wR_@?:2 Ҭ{ ߼lo1wDmJ{RPL WAYZӫOW(TdT=_T=+җoUw[^. Sg)uKYs#] AM1,Yw=8 f}v-(ah \Q١698Wٶe4nJ éWiUZ LD#UUr:T[TrJ ߿,QK㖨]Y7ypEPЉ_1.\3/m\\ +.-sPde q˩\R+TwuEU$`GeMu;4Q[Jɑboߺr7zhr]I ;+LOQ8r\)*%)*2.9Z}\ +0`kr L{qM@ pyփoc(=g1В 5QgªIzNCטzŵohI߼:6tB"&Pw]kĤ2PpC gv0 &0u)h]HUUbD^!\(α!D<0}3[{nP?sHPZ]ar .tMӭ8Bq z~w$5(ƾ(Pj`{c'fcH:&ctVJՋ?0EB8[teǿ"P⢘eS{uοF7.lp]$R+QOj;/7w{_Ą~MGDYz5=>uwoa Hߵ]Z;P(`4#+TT *HCOhU2VɉZf3<;KXl\#]16g7u倌ID *V] ϐ߯\wNȫ^сh=< dT}ctۣrKt*~\?w UU*dG3^) OW*/^wefpeSj.t t f@h9>VYmlG&^'Kh6ϐ#*; h:.25DMJc:}PҢwW' эpWcf>o!+D=Mkc c3Pe8I kVh $WphhyNr q_ob7{Aa70y%٧QI,ޣ*IrP!P Ēt%T4]6ZH .1lb7yk[a9WZH:+ut7sJ+,6u3+`I} s ?xhup0FO έFVLUOJV Hu#f~s3#UXQ ]ot)03o3ΐt7Oϕ!E#*> >$77L@ @Yg66]#?sQ,@Vc21J_>okL-$ FB/dCw

{^1TȱE'vo\Aluo(Q߱wAY] |\Roѯ]b&AUbnS8{srul:&G(>\OQ{W(T8TRe4v_ZOo 'P[mN*84"oTo:=%K¦::x\TJ * YIxW7k)gΊ̀occP޾Ƽfd;gT=jO6cTW(T(TTL0]j,GK0޹ʤUm#+F)/1q]ű5Ht |nx^a`k9ė|Gio{wspB8cPw 0$b*;>Њ^Р߇=tԡC P:8@c z4 ɕ=?̭ɝh>PcwU(,HBK| tͭ >w :fIc~E WcP]=̚):h ~io*tй*x',ayt}堬ԦuvWP;ZzPX߱MiPleH,?꘱9#oe:ƀxʈ/Рf`|8m -Eo6rVᴦ恂Α0 BqBe[2؛<03Vf :NkcfUcPyo)Qq5]7xn`">m'=n+C)" zy"q>X,K:H:iWu'7- ֐M{Rz\aP+P&+J9X 964ScRz9107y'뀓X\I} ET%Gu"eT5_IL*Ƕ2;9*tLPoPa9:,0e4yI \mo˄EHꮟT*x\s+BF;bXCH^Z\b/Ļ;rH]TkgG, E"Ύ\l>063/5pN eRkp"&W:Tuo\vnݟJ{"}ו[fHubimKT\y:ъ[mDbN,bs++) \iZ+:ő+<˕JsUS̠5.buYZO/)N ͳJ18۪V+k{lG\I82BwU=\{6ŒxK{Jmu\.jC!:gTSG1ws%%1^U&3oHZX5Sb\ D˛V0^+*vz`Bc.WuC;2QkxB:W+榦My[kEyE%嫛/Ԟfn1H:2ko_?8%FWHI2{O} =hK9󸜦s *i𣓈&j+dw3Ùa\YUE~5)F'Ճ=#R)vVL% *+X+s$ }~ 2mTq%<Ĭ%BLOkWfV&~IRe"슫 %g}\ ¿'Yv| )(S[Ycbb3mʶGf kFHqGVm<"+4u${wref_A'#?+WﯳgHx\iM 㪿؜]Z_["?sZ5t[li*t nPq5;or@nceilfUmISyW}`;S#8^\ߑņyFf1،"lЈD̝ɶfmxFC*(ħ(e1*A]]v)!"9o].Kq޾Aɫ-]9U*|C<1GRŒ8"sf~ ͞8:ds%o3~B鶖?8;>hge)F=Bnd#NA?m7+> L.78\eO%Q3jn,l=I`J\e$ƕ57]d`S#;`ΜX gV,-,/-ƁkyK3,WlN203ƍbn,]8"DDƧkL"%$~ene<;w@iůo5cQT%WVVJ)0AFy##[ gXp w~li~U-r Sz/ޫj\V J{5bv{[])x<8>1 O vCI8|2.Ijʨd#ќ0+guq lqlK~uXX ϻcpra"4 $r̨謆rrq_X,N 3<[ehηҫ}ٮ,"Q&܆Q)>8xD$KsaA~pT*ە ͭ|YDvwôܧdRt/U HĢ #qT 3xr6LyY._q$wv}92|BP6*.ƩN v|[rȾ#|>O=-{;r9u2Y( ;C)ɉɹEFWwI}k Tb ^x*2@8lmyhy{7ܞȧ&T߶/=#@Q/CZ;>n )ʺ6 xS|Xb/-lla"xZy|gsfvUܭٹ!pGGXCCZNMrx|ݺ%H쯭mYҊ`w9VBܼTi33 ׊HDɉ r6Y7>84,SSS@+Źx2;p<BO2U\ڏ}O`GM3Э3(?FTtpro޺H Ўw$7(]oߺ[-WYRLe;R1}BEhRws% Mq"\Espy&`q-n't7 :Gݓb 0#⳺8XSw\]K]~[YO[gpX)Ī/ur#yVK9q&fC ;,ɸ[멱0i*BP_BbeJ39H] cp&\; o\DJV˲N +;4p 'o^ibRy@_Ux>Hxj%$*/ΰ5w Say5>mxdqM,JyJTҲzpޞnͶ$s+J#&dnlemJt4hsei_XSh7 2 ʻfjz!w݉HQq;v Nju ߭!D14S]|\}BP_!FW>DG{ŕ60ah-d%Kq VU꼭Y^%LJHĚ#)L#9+bri3)4?=3ӿ\9qe mjQukezdҘ GճԊ2Mm%''d/ʞ@] \+PVV6[-$H 1|:\[ --.}IX\&r&ej ~U^wM!K.WxC +ǽUaEgՕR]T\561x|BBbllsyM>cU\|"ex.Q\-MzY^.š6*6g824Vx3FS)9|" 4;t;uE)L)=M4*YX^"mW4wmGX]URHOKA}LX\\Ո2י_*w Wq߼j幔u7p6E7k,ÑI9i=C;`)iymd1v܎MxY endstream endobj 238 0 obj << /Type /XObject /Subtype /Image /Width 200 /Height 89 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 39 /Filter /FlateDecode >> stream xmH@|ںF endstream endobj 241 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 286 0 obj << /Length 1392 /Filter /FlateDecode >> stream xKs6<8i=ID7D$ I -Cܦ1."%K?,ݥjjK,Sd>Y{Y!Zqu>䷰|> d[aѦlСC[`[Z6YXUmgjr\AL6谡X~ R}^ U]eE;{w;؅K<ȡ4f"ʊ1Ołʿ;tikl0`"D iZ$EcO> [`^W~)+^ =bWY Oa׌?"Cr03OxzOx!'Eke<*1d=|m)VJ-P" EDvc1m|u(]34TV_U$̈́yo|6@S[GBu͊_bgbjL# ; _lT »RCVc:.e#`#HqNaf~","oNoaQm`Uؼ5p&e%7BvzRd=ڌeyUV"o!mi@ :T^4[cuMjE%p(@n\N`)_sn`=!zڀvqw1FkwQڅsiIx|p&9B:F(`G~&eDL@`/eY.n9Exf{ 1WaK,t"K~s4'66QA}Pj" tga>JxJΥ>PcLw ;VW*1]D\fw} 6!àī`BdOFgtGc _TRVlslz"I (ׇlXFA&* &>6Ae-FEK:=",G&pr 2 &*xyH\KU_Omsu(',.;GNt]d9"`|f6cVd'Te6`O%P}M_o^ x[@ κ m} ,J/Pj&NR_GJMY]3u Ri׆8V5ֳS>fVdɞV)sDTRJ(T0΂jFEGb٭ii^D0YQxjL(yq%I<:Řu}#4'P XTâ<Ԅl 2z}Ay ,hy"RFP G`8_J,L<*ck9{%@pF]^eHy endstream endobj 306 0 obj << /Length 475 /Filter /FlateDecode >> stream xKO0sL1~_WZnVqhJytdw!HPQؖg(<U@pjѕh*L+%"t]\Cu( b&$ m&ףctHXR+v5'{(Pvcƈr*zwUl6Y<È=ƴLzFQIMl`fu73xo{HՀTn(3=+a0*b2{(xgLq1g-﫩ֻ3j{y0W4dTAۏ$0פ@sHpW[ }}L*%#\+&եѾ rwNq*>k endstream endobj 310 0 obj << /Length 1145 /Filter /FlateDecode >> stream xڝVM6 W(ԬDQuLaMS-}q~}rdH <JCD .R(JEϣ[|D;?E ":z.yxY>,^,~UTEGY RpRD])_/' ~Zf+* /\ٯwaPyTUyIǻsp/Ze[%+؛ t?,WG2^/ӘpXz2ey 7Vz9IxM'솩kX);Z˶=mE&{OVFYAW,5ےUYFfނS7*NV,Mm:KչT,+30M<}|{y8/gF/~xDgeyZS(2 dvC+O" G<+^'b0#XOzV +U-eBLwNHiԍvvͬ=,S@ȗHJ$vΈV׾MVU׊;dax4a}09%) Xdaʤ ׻F cD99Q;+Z>aBk*e;ԅV)5H)Txiq1]ͿY`(j?9Tt,My'\: endstream endobj 316 0 obj << /Length 213 /Filter /FlateDecode >> stream xڕ=o0w\JZ 1D!hPK'߽{}8> stream xZ[o~`}pG{+$);匣M9'%C4k`mp7/Y>c< TTlx۲΢~fwiTnAi??fk*V_DR;s] *ghW_TV\N=~5Yl&f+D0R H򇪂STHEx l܌ ~3Ip0 HC L{/0^(6b"qQ`tm$m֒H*4i">^Du-!r \}@\\/6upҀ`1Hn%%  ^\}ƹ;=ry zfߤ PkٯKB~!h,ED̫A(+*1y.ȳ:R؄Ձ>fh\Uƹf 1;!(gWr ͳ_= XHP(vs꟯m.L7#.6ʊA3&3H`ޠ.[]ZEg@ZƲ#}>b*D.4GւQ$J봣:kPC QLR1JodtDwLcC!l)`ņayj̥/.m93,]&Oi0aVU4ѐיGL6??0c3#UIqMe9@fJ=PS CQ\>ME':Ad@ 1ӧO׾U$I}k~}ţm1Sp縺baWWWAȀc} d&zy_`1q$; o9)v`X @uPN1nѧbVjf@$޷erqwP+ū_|]ePʥJ]G5)({vV*) ;'X'(o sUd?S0JL'^T7`}[WVsH[ƛ}MMeA~+l '9i)gZ]̋ |#1BhȦM |4b&nO۾me m}g`ݧ뺌Ӧ,XSe;a9ƘPsjϚnzh5Sw0fV>94RR(JG-AіdX;b 4mtiYEIz?/S'5 &@: B?0 d'uO -#}h/r49:MnFy@3%]/21+5:VVp䲞T r@ЃQ\"^LrDxP0A ~G`ݥŘMvCo1#Q'%G+(Eb(n$l}|j{Szp(]^`ex4e VQ]"O*ÍXK .׿ .X^ $"IA k.cRQ=vJid߽uuC, bw\_2 {3Vu;`Vv|ӓ#FN@*h]Nx1qN@H&B=؍^̯&wa" HTg|``PM+:x"+;9;ˬ']<(dE{dx 8EM / Ot"T27b&8=d%aiyTbE.td~"#+6M0ү"q<`DWj˝6%G=bO<]əԛD:]D Ir> stream xZkoܶ_k.ХEJ"iN }- yWխVpHV~]€(r8s %׹t\绣gG'ȉX9[;pH1Oy}BjmlI^uZ_R߇$K* $?}a4d: 9."\Q[Ǘ0vrk$l]& SݞϹ%fxaO֝Ca܍WԢfU5+U$˨ +sZ9 w, : Y$%m3 i|HӘ H3@Wt2Ωqa5UV]oԦq]ִw2].ᆪux|O!<)!GRAoOzqfº,#_@\^qm:֚ޘX.7ÕnVܽu5譻/ +;0]vEi~Ii="LuQIY;M  ƃ`:ެ!̢sm?[f*!Nm56iEuߍ}@# h4 `6bn9(tCIzwp[invY7> 灜!Gb_ޜ?an ƀP@9l6βk]M@Q*&Eh9Hj31m&lhz[6uB~_4wv+ гd=`&3%"F53M1r痖.-= ?x{\;@?~ߟxN$K^ZQ*B PLjg}d`\E]jCIl^W\5JN6t ! R[):S=6" G 9dIM !YiezaD2=/h]_+N㉰-e $MxC58U %(f)>5_QgkW69*w5u=R?*7O`DzA..`hL&Nvv<oiUa1JbV Ζ1$ q!4FǿGKTË_LKFM]Wx(p*Ε鼦_xl>Cf >-kT"Lz˓5#jm}?{]jE. . e`Ӌ@}1"7]хQ, nlEh[46 @(;K::l 6`lT~Ye[XvܕIkֻȤФ3[\Н eh( ^ֆa@0XjShOv(  #x.4ҴP q+I-:3U.96DchR$Ʊ2Ϋn 6AM(ChA z>$]>ѕ"tO`%83w }/eMIK0d~1p=n(l3iJQHĎ5ފRs$&rp[]/U,nJA, mX(ٺ4zpN^s22WuX+^+,Mwgv5Zv2;C>zVω#N,[Yz"Mp/jL ҁcP/ۗ!@s@YwS&物ܮ~ćب}`_Δg^u1hc䆱O= xЅ}}'O\sSW/pgݺsa@dH)P d_9=$YN}r1$û.wq;`'0|0ݼ$޽9ƒ<hDMo@kl[rj GY(Va{ ܜk?rw/~^?V)87tyų'fGw8NTUv* \I=؞}?mU.ŮvaTa'ߌ?CW9z+٣s?g hmubB_ 棕ZÛt<w_CC<;i5}!ވ'򭓾s@r3H?CFS&dX]v}oT7F 8Ώ˟PNhT͌%s%)+>Bt%`#]/Ky}Wx/uDOdULvIZ& OeUx ܺƼs)cMUzXc-zM߱mL߱vSH  FOǪ{S{0.C.|Pri_7`+n}c>Kbc|0XS|X<3sѫhWxGw*!zF ~HjE endstream endobj 204 0 obj << /Type /ObjStm /N 100 /First 881 /Length 2441 /Filter /FlateDecode >> stream xڵZn}ࣝ6k`, HVa4 sz(XCۢ.U.wj.U4Ŕ`hPF#E&G[h3W||4&z^ 6'OEaM7 PZm |lL)?;0Ӄ^<1KY/_N_y +5'>Yn1 ù?'圷8?˿/d:w|׫B0Ϟӆ jK.ȮHr%du{|r;Q0tN6666nZ+mbF/6zы^jRF/5zK^jRF/7zˍ^nrF4zeK]Ll?bzZ]roL/2?pMYO1AxFQ ^ 8;lmv.D[m1F4%X!@6_eHa\SXt&  %vqjL ٨6t<RmFC6D_,a<.D"(a#ge8(H])4g PfGY0Iͱ'2!Tl=q@@ فG[,7렛ԍ]  i0ui., Uy48O!zŒch)V;ݐO@NXZD\OTt0Q\yAb<*;KSehtsx Uxwe%V5 ac<*2PVEᣎ=x7Ą078xdDm&c5!D#grl@ƣ' ꬹV!u2M(>Xx>eHXC"0!@(C|ʌ[c$Óupջ; U}1Q{:Ve-=(~[wߦݮ7ł,=I5oX8|2x0{yKb9!]>p IEvOUķDq̛<Rr3Q:5šŚ-Z xVىXOMoTkTÀ}v-"-Џ^?bC9 endstream endobj 340 0 obj << /Length 3678 /Filter /FlateDecode >> stream x[{oߟBI 65M Z$MEhؓHA{gvvKmܙofhv3f_^/T6XxvI-,R&S9Z~ݟ?\$^vE]YW/ib[mAi/󫋟/8f|XQ1%⇟ 濚ELf<)͙HbogEdٌfFowb`eIqJL1S#-+CFu4_7FzKYZ; 8?`q"B)HNr&Sj5k|xe)WYYbUk .a2 X/ta T} Hu^}v3+`D~aXe: ]T.oτ}mDX&x20u1? ")-Kh3~F "Ktz, W&H؂pq!}d~)jhcm:]/$smn+v€ZY{gvo-9S"7S`u@q K?SzLl$9Y!: 6aGQ#0h IJy6X}mۣ@G6jjZ0"<33iW, Ojwj . ̐@/u.o+​m\>,|>bΙ;P4p ܧalSIh t"C5\Z* ˍ/FiY5"qAz^PW}(c`vKVs K!U~c.F,AژA"[0 92חֲ)αPoG6`@I" (F'Bs` "C|3>R~.:4qvba(y< y5!)/9oM]Mn ָ_ڟFcqi&PCi4<@RWzLmCcqu8j7`I6JCuIaUgh<+ݱPFmQЭnw4 0Ս{nO YCSQ_3k𻢦ߡl7 ;G(-Ө*ka{U~bAņeJNj YBaԏ-ơ>ļ_8"e}.?A2h>oߡe*NONVr"{qu ZCzqJU^Q`3xѮ.B#z:;u.ESgW;Q@{Jk"ci(dzdq>b}n<Yq L) SZVLqL{r[0 5VI,0BbzO_NR,K Eb@k͖lTg }-e)mlѬ-^E ,"azkBT0 lyUv ɉc!B܃x1hiIGFp0I-[5Q *6z<#A@dMnh%.WjA} mh򗲩a{:7aetSlX7P8gi$?͢·)Z7kKs=KI> LggmSvř[ 5x*ÖO2$)䅗ч mn|ts)kBH OinŹ\+ E%A|օhP ̜jq `M|pߑ~#^AnJ{%<[e)\Hu6\Iɗwrg1335tP-6%Ee0AN_EYUFP*C:g- /Jo'AoD]_Ga ͚s 2 ^y 2γhOmK<ts:C-LiJ[Tf\m|g';QtgrU"lŔTBjWYS!vã;3;KbciwzkPJƨhmWvAd4_*̺n˶?X,\A$I"@?/`QYnN)4z9_G_ }JUF>1s endstream endobj 347 0 obj << /Length 1738 /Filter /FlateDecode >> stream xYo6B@jHJ uu+Ţm4@zZ5y~ ywٻ٫_X(T8+ǣ]:r>?bA=s2-2җQ&2,P|avu=:# ;pd>}NFn;wza 'gPBS>gy^fPntن3*J~'/5Z!k >P`QPuB z*:u@+\:+lUWquwAwLo޼XfM$]'McOhWk5Z sx A\;FG7aP0p$hm$nb4$1]ERts֥kCɪrWO<}؍OvLPm$s,v#aDŽ!f+^ψYX9 Q#c6P˽ 5U;UVY!x*k6DV|*ʷ槈Dvb'YFϢS AtJSip- Csuu0.,Er*?[bA( ɫzPT'gAv1kzHd?XiE)wi1`~BuvN=2mۣ(WԄe,>lz{?iES,dxيʎZ3L.L]j'6 nżkM}(,|F@򴮭 lh,VGC'&c1=ߛ_kyٹnT>7U?QBo`nT;/LZ6azXT۳S ˢJ=LlHuAۨgCO $[G/xrI]4YL Dh HLBWUR2_R@T0Ur*P]0N9c*DWOb:ט\Y~oT'zZPdy!AI7e\Y +M%Bc% x߅RCk_Ui3!} C7EÍ,׎xܽ- ղw(&Sw2|ή >ѹuJH 񷺀Qힰܜpŏ {p\A~?p1>?]rgUUr]Z&* e4`Tb2n')􋽧D,d,$|5DE]34~c"ʅ a%D p>\i :p2LF@׽4;[Z-X^4P#̣NVEۡVv݄n d*d8A6ND s:c*g1  ؆Ϩp@>eOpAC| ffe#y:l L 9GY]7:N>Ms^ le~ *>̎~2fP ڹusM+bv>-ܴq ˫# endstream endobj 352 0 obj << /Length 2426 /Filter /FlateDecode >> stream xZ~~poq@:MA1 ēXS䅏.u%]rw8; EM@ܼ I4C Jb"bܭ~n(^Zul6pCVdi '姻7n~a @QDjw 0>D$qlWᑆqr쯄p-N͂M~$Rss2 8|c(]JHi$AV*2ҲkV8//jK5yd&;zʕQQZ;gb?D:!2)'TKI3/A\iY;&u$F*iTE$Jvj B(Hi#PAԎٳ=Ԏ9?W>P\SQ$f|,SFM`Ц T.a%4 ǍXB˞jze${zM,cA"=d6ofz_u8x΋™ttdj}٪uZeVnod6R|yVIq#GƄdѪ'?`qﺛjFHA R Tdk`5 +p< n0bix JC5b>K,C)-wZQ\-feҝi36DY0 d$J!S\;A,/;3\%Ibp++TJt 7e–6EL^Λ"MY/ksgemyl{_ĥN5 vdmn9~71]+s2OYy o| Ga|UD6mcD6cŮ'w=N t]4Fq4HpQи $(j.ՐV"Y3#IyD YD-cUiٚud &}^!2&YboFE@GzHj8_ MX"5рY.$Q"of騱Kd2D8O5]ΐ %b%/X:0FuVހ.pAh鰧G0vFC/2J@ů‘jpV[hP 6 ġSlkIhd:dJK@ɝmQ 0@v݂98U:,"SYhWDebթTa9Tgsdf(I?ԣ 4}  CA_٘|kGVuvQ (]k 4JRC\Ȇ\ݟ%#X/ƃe|G='A}j&Ma Wt5*$<&B!RDיĤ64FZuO5CZ1?**8hD8?` E'2uW Xi]" <0}m\#/b:'?|s%5|ȋ kKh8O $.qv)^dBC%]?|zb(d' m~mupӴks0k" 0|n c‡z 8}:A]JBzsG D1;9lK^*JpMcZ0#[ u]n}۽w9Z|UaǤ26k='F\lrPOD Gpɭ\%,\]h"mWY$>(|#9̢mnA[?Lڨq6wu]ՖInsќӾYW+8{Ӳ$;&Φqu<4üi7Uv"fFX`-X0BT[}^:.~v~k\ :eFtu`," ,⏾)B9r)'4 ,dumeLd?A< zzx90E8"&p%4V- ~U(7zfWӝ .>/N.lP*VHo6pSI7REr\,VZ?+ %X7SM3LvJCV a?3_8HH$F,V endstream endobj 357 0 obj << /Length 2781 /Filter /FlateDecode >> stream x[[s۸~`}g,w^y6>dw2D[J4=%J֌EBp=$M b۟}91mVIVUgW]L27 D.~{339QVgHwUA4p u3(l1鞒pĸ5S"D)s$K/"^%URX)x2+#,ooPAW/EZګ䒈jLl].\T\ٻ|c/b7VZEg6$MCeIvL>եri̗v7T YJ /+io|gӥD CԓB fHF IF'Hw &L)(E|ůe)`^h؋/hEp ”%[S N Ipc{-WF@`HfxdHa.3cj{g {-ƀ0F~lշZf O"s۔d|f_|fοf۰[2'n (jtmtt{3j!Sx [:zXH1Ŏ+:L^f`_<$t<|(M8P>`eM1a3p#zc\69YU);W&}g]x0?(iyzX0ģ0aθNBv,nU=9e2g7ƦM4 C"OSc&8 Œ=8@VG^':0g&Z:̹j>#Ή@gt>ĦrGC HX9.}XC1v-u ObO5{+O5}*C59/XXag]n{}ImmBr Om9/݂PD> [{{^xC螩ov25#(DO:/:6ۏk ;.מēPY>c8A;w5 >j*Ur %?mn߆ed~yOyr Ph뼬[gyViVlބʭ 06ov+0ye\7,ּt8nC;H8Xe걦ꌫH_lH",-9LԤtX.ý.d:뼈TRh@m`)CҾ>a8x&fi /l14 e_ڞٽ؛=(Tlƭ(F$a,QeӴ,=UVmJ+]sm~ : V:G>yYAa4LAUjϹkR6V=A+nJ-SoROo3%}Q%Mat9![DOsI5i5$E^<ϚAOS﫹W-w2P>:ut;9 fnj2~V/4 S%3¡‚piǢBYi](ai"Az81bIfY}D ;}s8dW# 1ڇ2 Jh0dRRYF쀆 `^_9Pi.jjd |>&I G}&#<:˼U0<ډIHSR<]SRRvR6ӸLgsԁ^ 4T׈éfpe{0]42Gzk|VpDAhOrMAJ́V vNE/ endstream endobj 363 0 obj << /Length 2771 /Filter /FlateDecode >> stream xn_ x v$MȾ8O`hD*$(=sU,w[,jD9+wv[2 `aZ!Xp>LS|bVirGhG E_oޝpsmp@Dï8X#U`Z\DC ৳af`<)I[kGKj(3(1:( -7 >LƓh_D(YD<R4)UHFoq'],+Cqr<]%?'~5~+6bEF@pW} ,ߊ}.)a@GnE߬x^R12P G2Xs%-ST)X-Or`^L9W(T"4dAPe3`%ZE юfZpŽ_7˥_Z[[gp%kyEt% <\82%iYfVQen|=Gd-% )iM/ӥe |]9UFV 0Q>IqNjȉ/l!B8F`~]% ;1TjiqM*{DZ |aVEJK0!'C70R" [۰\s3liE%8DB@ uxبOlk*lV*~uŞKdCxtZ\.4Im&*HIî(Hp8vR"{}8˞6fXt2gŽg("1K y4OEo88Ys!b8;܅ZNDϼWfH7c!P~~Ouw?ۣdp]U/)͵W:Ek aZݶp嫐"5UMj22H`MvNwq znnCf!EO &@~>Q_ QNg!CMsTFǿF4cG#yBƽJ]'5ŕ!d +6s*tu {j%b1lFL,*pwPo]5WuA~50V(;nMj$~ue{`?!FHR#S֩Q5#zjW [3ѣͻi(ThN_"BRڻS֛C#gE{~؝nuXaR~LE"C/W5Qlǘ%vт5zV)K^4kʇ%];ocR3mmZ'ru q1vҸVVpUgX?ƞV=T;os=І;= 5+HQ&TjRҟukaT Ӄ{dY 1\L`. g@茴9R/900eQMC(1'$AWG=t3, ܝEskww4\uj,bw u_ƂcHU:_4Z'1!?[Dɕ?"d0/v8ol 3h1qKǂavX$^Y47[8U5m0ʉ<:@xx f܇[0zsزGXe5qKH';GU.48C]qLZ?[~dJ[m-KɃcQusVEQ3f aS4Pd^24DTn]r۴96JDGtgTi:}fp I6JJL n%A ހ endstream endobj 370 0 obj << /Length 2534 /Filter /FlateDecode >> stream x[[oF~: јs'ŦyJFw-R奎=sU- Ms8̜9 }.\\cPlֶe\3\ZMhSD`3f8x㣪Q6ױVc#e*^^ah5Nla! lnqV} E2lc  6 7nmҬOY!O5L֝C"k0sOy;3L1 $88ٗE{CU ܰC!sc',uu(; ` :-K9!t ;K}mm4Pf16(e Ata~<3`#/1/JV/?޼[?|h K7uN qGg?m/2% <[}fcpKa>-f9TxJw:2NSΎYXf-RжVѥKڇ|ج;?Ckg-5 ]niIoN^'}ioUU@QQ6`Q|BGv#l_|͐%bb]d۸k+g ck&qgr 42 ͻy6#4j6w ?yI060iddGYڜK_k|yy:SŇ3xbG.J!*D;*6;\

> stream xn_}Qk¹qQ`& 6>E@KcDjIjUlٲ7#CU>?zVALEe%'<5ݏߜLғdURyvޛIJ.|/GЄ Xǟ`X7e $%LE^uz2C}qoo,KbRd@0FD! pgUȗn* _|]#M4;=" %B*$$,(Lp Lo2+H)Gf"P""y^(NnHBOtvޗo4I$1(2KAenJ$_M`q"Y~*MZ`JCa/ KLKBA$ӈP=pb&~{ Czy1<}( ՔW'5=L`L !.%{DNߋ{pBO٩xȃ/Cy!:'wfLjy<˗r_ &lse]-e_N/N/!)'bP)D=vwsg HT7xDl;^|U1At苔S(GI0f[h5 :GM|e_/hwo Ǽm0N}9jW4DoJ9gI@\e&&_|uLr iv$/y G(ZWfoMeC>FH `RdRUKin+8>\-z/:͠]NMX±@XŽ|.;P T՞4$k@4H+OSF3X)3ۜh>S9=t#Rl#aP.gW'-I8l;rº/+e; yZgۤ:>tixХ1ȳtOlvPwC[ՄC[ y~W%fIu2B<4qym Gg|sx?ND㘷=Nv6m> U)<ջ{H g @ ?6TMV}lg#Fewɵ u6̥ jpdk;ȮVZ=7CiEbv Ziv mGML${s C & iJU p$Y)*KE"G7fn9(ƌ Wj,ɾ'̜.D crq,E5:q =캼׋[_=g%}DpUyq.oM6 {qϘAJJ{iyƩB]2"4kjfHF2[M8 p@IH.>V0Ie[Gy]!#U:F3نFreETAAJJmS)%yL' wݘ3GEzNHQu0شC;¦Rrܘ8jgs뾼HS3Qx n919 kǨBIKO(vfIUވMMlU [f4]Vզ-#.({Hj/RDy`m&j1|1'=rV}嚷u2Y̔%@j-vܞ#Q)8k4*IaD4mT+W@SC;8RyL}"q\ϱt|QuQG#p̒ue2+TS#fJ/v;._܎( {pބ6"v,Vc[ф2ڵѠl3#UR$p3W/\bߠ.y1&Ik,CJl/Ɣ5DI>P1NbJl7[EƜz 4iiW6Iľ]9;!X M;*(>R=+Sȇo}Pnc{{f+ŮCꃕU|7ϳ"ETDŽ+e({,Slx]Pʾ27\aޒeX S5Ey's;SQc8ݖ3ؙb*8q/<kdF;u]TSXR 7eOق[ƷJHj0ۅIKD砷Љ&d{cL9^?B>iy1V)]WVUC Y^,Հ6 miwUQݫ!+nߝIT䟈?*.PÓac˶+]86n]($|q3ғޯf@q20f\/̸WEL˜z>0 )f01fYcEr\㼝2DuofO6qf_6*N\bK'f~'ԟ‹u#ZXy5=o(f{ > stream x[mo6_[3;UhCO{E L[[Ir;W+, Ejgf8P=xX(Dx$ z+p5'R,[a%w7:̴ o/~|ۋO ?z>)s˅+v=c>_$ΣxW4.W~}z X9'Ǟ=Ӓ^J"n%"zX<{p  S#+9$H'*7?H)`> P)j.| iaKe?ZF'Qi/t\osvx}N/|LϻpY|WmG'$,r0./\&ۅNm;5AYBGPPѡJWIld/(>l] ,@> "X`gRY'٬N@{(Y;rccd9`!Oa 'VgY lJ_na(R|z d.QVS_q^ ` FQ "=^kvOM_>/ٳesLFy]7A)}2C93Hw.$Pzlh# ou"[k%>nFCRYk+Sp%Eef; ʸ5LTC Nчq\(Bqȅ?Iimjk|MJx|L\%NP9$EN`4:gz2T:*ш[ gÓD\Q+i{s ,Ajğjdza_/׉M-ϛJηek[f:c9^QFwӐg 22dӐ~yriȞX(OJ09ŝK܉؝sJ> stream x\[o8~ϯ6 ˫H:a $8YvCFKtuMp(1v5Klea0Dk81?x;k,1˅mHH U'hBL:!3@ޘֽ8ПKs-hczίeR:c fP\2ĸZIm"`POiz&Ӳ@1'"ϧq_Yˡ4ӥFpꢥP[j~%_庚0WY7 Z+U*?jmC4YL"dBp#By R)`S)1a#i];m!;&gm5(|ݭ9SJWH`mFq:vD= TGXtu:AAj 1I|;1Lz1g! G9}Ώi+ '繆v^IrN`Dz̋N6MBhM38o:Dl3[.ЅNCخ.$C'X" ]L!.P*mL ds5o}u!:SqsjEM;>lGNM} =϶iZ2hC3{~l6HbCxzIڽW:|( t5 ѝ]󻱀z}׋֮X5K6={A0`",ãsj8OQz!POqE5Ա0J¨D0!c}I=E>ЍЂr@.u  /2) 4OUs͡[|5.nybWISS0h| w2CgC;ŏ^=r{gYXDzh;1(=Q{jܣ4q/g{w}nϒO"nc_h!a7V&x[Z@VN}+;]nBHE,䝬ühq;_$O/ znس r>KŤ^-uxړs%O4 (ߚdFN%Ck!ug ?}ko|x, =L|@|W'YR[زH9Ypa_Tv=lB:{Mű{[xvWo5N]B!䐍 NoQy[Acx"DMz:$%Fs&J'}]Ӱ~ }7sbr1˒ )PmIdU:fj5HHQ.>ţO䛡;@ ma9zk IMX`Ϊ%2]P"P. .Ҟ/9g "*TEW0l(U^ p:S}N:DC= Vb.Y S rӉJ9- bqanl鶐'I"h8nQkj0j=hfٍi$PЅ;'p #^#6f6?5c$/U}زljok,mzQ8U:sYS)w&W{-$a{@wBDHY36Η#вxչy5)2^ZAGi{,4.^Z3Nkb[Rց3`v,0ru:GZ1:쭪oCX+l!-SZ* 'wo^.<_n8E85)dK^P&R;F8j;uC_=. endstream endobj 398 0 obj << /Length 3050 /Filter /FlateDecode >> stream xZs6_˭YFNΙkuqLF+jV H}q%3^"A u?}{yJ%˫ A& .7O~|)bZY٦m^h여&`?<ab:0OAdb[;k(͙#h۳>j0Bّ~(a!G&"i1Z0UPgyHbB/Sڲ766FV+fTD}K6K+HCHz@WՖjn1IXUxWp̮D"njĶGtL׳;ZɮΡ9eqGinځyuKJfb4+fBSvmlf,f^pQl,62V5f<:XEΚcEtI@&=%pV{uH&q7P&UM/64rswi1JMV^yvS?1(Uᑗ\hNڝ<-s\4. ܠ 7Ԫ✃atSz6~K .,{gDi!iq ֆmotR4XH-s:/^~ukhUq?Ssڮf n=uF[޺uր9([4ҋy (58Xk[봰 >EfXn`$ wP+/̹%_j%=k#aRNm]VnHL*_mrp2Q 0|0sFgB"~ *QH"#.sy ppcy Fgi #m1fPڨ Ա,NG, ;s8kB'n)Wq'W?6f 呐J{5@ȸw6-Q+;H[GOic+w"[Zm)z͢u%(8rHp@ŖHhI߆~mn`:kQ) < K4<(AV?/@x(D6{솧|O#Y4{ :wxĦ)= @ä9!CY+j-PJ?u$vNX/ %EU[ynq%7mt*4Smj mx #S5)SgG}cT/2PF ̈́"Nz8Ɔ&8:rRt'~{Ͽ^s)hթUyE)0HOfG>jGwEY\p_䛋GEUd+eXl&l>0D⇱ȖxWXUgzlM…LazZ4 tM0"ck(µ{<"⬄Bc>MV@1捭:tXvr$860#E?oU]m@@g-}sIDH}{$xF\OBnߎOڅӒb 'X"at*=./ Z!{Ȼ (.'GCtFo17]]v|ȩ$̇'t?!2F Eo=:).*d&INI`Xcfs iyDi1DtFa Tلrco-8[SZ(Rw|=qJVښ$2;Х ȼt;x} &Zj[n+kgZ5wMmj٬|vtv/> stream xڽYmsH_Tup%ulRIvV6-/ ۹_;\ ͨ;w^p{@;gd}O,@,Z;/s_f/ȉX/OLP{~9irӳ7f]X3W* =rcΒ*L dDkz|i'ON_L]ſ>~K|Achz*.&ͦ),nˢnIuY&tAwM4L۸,s++֞p fHƐ7~9"9Խ^kTpXK\g]*K,J~OVE*7 @q, +|֮>4iV0|$awNI{Kx` `B^hv;v[!]9UݰIo#PhG ȬИ5Hu!=p\H/|r}' n*]ǔWS ;gS>9t[~X%(Fo$FRb.p+)Di$;̊MCC yaעA4֌4(+IXV&T@|ZYoW,d#F?/)[4P ]BZ6=+k=KϿM|CgOfi@&5 Ow].R)=䛯{ 0 WEi`䲢$9-F!"d eD@#'Z_q2¸ A+=EFv'nb+V}T'rjl9(k ߹7*.:m\ ݫ}kq tOUY>WoF4AdM-HwZi5uIcOpwĒMݔ)Re2ݘ{fgxJRYQ:\!ga}KEROԐIM :OB]@D3p&-l&@x؋'(Y<@y>:,86hEsS[" &E;8Lba:ng(A{<{X>&%eUIC7p޼g ٯ$8> w?8Ձi - ~[qz/#h/ɎRUdORY*MN-цXv戜2POՠsmo5X׷=Z?;ϊh0./-}}̨ݭm@vdZE~W=-o}[-of endstream endobj 409 0 obj << /Length 2501 /Filter /FlateDecode >> stream x[[s6~`> `&q'm桝uݧ$%:RW"]JMHڛv;I;8ǣ.0Q( .oEj4 ._dHNGydQ6MS;vh"'.pyp@Pѻ8#yATI; qc`/.~y?# HqN04 D!X<\nJ&!i%8\Z᝟ %ƃl6M2{3&i*o>E޼4-\Mե0/=7үT",vP Ha XaChpT<; f0L[ݽ9rKb ~~sB ]!TdC}!XO-褆 9F&9#NVױo{LiS((M?5(#Ƽ;z!ؽ.M_<%x1-&-(#$bѺ9"l+1Ҟy[m3z([&)2rNJ{er-۵И{8I /kۅK}<2 Z0`%䚭#&Cir3ZMswʣKd 6q@9MW@Pg`U>H. aY 'ђ6a[#ל5?Yͯ;}vraؔv6:BA Dn:_`{xINik gX`8Y5$Շdvvo=dtKi%>;_DDhva-Vg4z* IoB[By͸hnOo41ue0<åt?FӪ)VfRw!NJVbNGg+8W8Fv5_Ͳ' QPB'E Kr7[z/x)>7aH)V#K} Ƈ8/xZUBY|S|bve?vܪkkm嬁P- H`މ&oKTu§tojMDۮ(o\h!$҄{ }aRe56N$7 WHVI#.J^NA$E$kM㖓xjM*%eV@t6qڿMAS$|.SMhE5V;2q-/+|/K?:-AwkE{A4"K-b뼑`YYy#y kθ.sp'&{S'NI B8lo^N7!COQ?i4ѡҟp57(pxw ޟ@XhzkOXۖmn㇎(*$Dtחۋ@]j &s`i;`@Q@*>o+>MlYxMuԋoGT|"esѐa*ItkKTzk^ N_*Kh_^* endstream endobj 415 0 obj << /Length 2378 /Filter /FlateDecode >> stream xnF_M -9 vlu4̙ssFx{}J&QBut=DPʼn!ˆz}o޿jfe}M?p|_/lC#0J8.>LGDwn,kgO?^@&u>|Wq h $1eȀє(!"_Ne1yi\~Ex>n"?iqB|QC$pb•4DJ6qu$1L JeKMbxe“Mb?6 ?ۂWWqs4+_9H?2٥- nՀQ$0!fT[ .XR3M ^@?ӄjБAXΈbI5 t@a f(_nlyʖ-`Cy5h@=,? O&FpEHOMtGfc<*G:'#L SAhQ˯# Gp3Ђ{h@_~MG)4ksϪPP*i2p'm_L~正pծfz=B{`s>fx<ҼXN u_Qy<~9dW'^3םOK^z|~qpw,*v.v&hbo;?s[RQS< &ab K;vDpVf%3pκ + v~Y`dyDfp.=CSN;bCVSͨ訊_UQM~t&kDE|_Iq"?{<^2>cá`B/: 9mxc tQ?\+m?M֢{@Rll2,ڽ^Ȩ%MZzXㅀ%߬Y߮[ge&<^n ulG5Tuf8H 4 v;‹BЄfi¿m4#t6qHLПns(zx!xd&yu{uvn ?lŮ1 *vaO)=@}]!]n5it9YI{S u1_ĽwE & oo~j^ $u6hG[;LC ˜ j ;,(`!ֆP~9g @?n0.egwIbr T.Ci6\e  g[~k89w 8E@g7 !*`0c/+GAs:~-`/[:xsr5Ո:AP#rcŷq\?phiS ] u/ ک`uX!-6c<U̩R`h5mSzPo顦{njvm(ynjJr T3UِrHȈ J-W9Ɋ ʗ jz4Cii CbOc~* a&\ C Sw1tQ!On͛АŞBqe[ɿ+1bB E(ӄz~M'?&i ~2M$'҄|B[(ҮӾ%HREs:=ѺsΏ s:e3(!O u"e06IQkι96Itmq$t%?ɍdHv7K qHAA?N1mODlfdnܬ<%gV?FśM՝&]q]:j|MyzSr4,/kkZ$wc a$َb*aN9]uJvSjѩ&)v.~9%;?N|X Q\ c ۉ~jVqͥ435i?}1 endstream endobj 422 0 obj << /Length 2521 /Filter /FlateDecode >> stream x[sxPk 2v3cuik+܌"Z`P=$% Gfwa']_pI$x /^(#"]&'O߽;0盕ʪJm*.A@'.O~9!{RHЛN>{1y_S+ hx$lр7бփ!b,b93>y4l5JYE :ތFHDF*]Ս$őrϣ3l`D&0  \?^)!Q#$hGҏ2IgT UOԔKl^'fP%usaep87>0Wit6ZVMZjٽaVlv<ϰ7*TuS6o;_]% nVy^zd &@">T2Jcワw?.󕻖}S֢/nD"vlsv.NxM$M㌄H2@oK0yߘ5fi;BVkO_ٷzs8wx~Cֺ&ʑgsM0s8WeWq5Q5QZkyܻA*[6"- T\H#%UH坥*mY-ԯ@E"Z+  u.͒/M6n G^?^\}*G5+(bU孎'UC:Cf8IRLKTA0O"*O=PYF׮baۖRUN2 Liz C>dC@#XplaIm{yӸ @npg*uJxjKU/Y1iXC*%BYy!b!/rd4Y[:v:nJAFm1zma#}DD?a:9 H0:HcI>m7]?\rX8J\?LwPJH85VyVE<h"߀1Ig$$lOhf9ݸ[eyݗ21gpt)D6zdrWO5AINP_EzWiVMζ@ G{Sd'%ԅ$}w)z:qR\Jg$j6ugfb]joz#AȲCҔgn4U:MptƟgXҲt=kFL~ |Ƀ,%۽6u.ux7֝f!SG3ȟ ].NIKtgt??21=c 7 k}KZBo:| ^tԬ3:B_L;mX' :psW4;B X.v=vmrlFd`xb@ݻt0c|`Á޷Of~NܳH9b!"BIhcю&䇕"}}S\ڲ wU^8H6- r0je=!b>nS+s>]&S#PDS-aZݲǣ|3# GjG8byGj˰kp{+{"B;[;#dKBq FjR>|T3BX4'5(a!C L9D^*|̳*͚#3;bU}"9sϑU :4- Ƈ4ۛxmV C hoa8+wTy˞I> endstream endobj 427 0 obj << /Length 2141 /Filter /FlateDecode >> stream x\o6BMboRE7`} nkO[x%ϖDI5 *mw?NGbΏ'/O.^sq+t.G 1,j4s.o7>u֧J^"/i{O|oP$9p_'q9rӓ>`goɯQ%y?dc0 \*Lb8H-#7 A47S4gT>"m}b;9qb*[JKaB$’\(Ds&Qg;#P6W!`<(.)\TC>nڪ /0 SRcЈ ٣~/1r8`'abhX遼IuSR1HƐZj1ePZh]DɊ%(R+QJ]7 m6K".BRYx2WF,hdZSMj Oĝ6h"4lXݞ \"Ast7/,vMV&V ;'iй.p0,ɲ&مx2PP 30yQc Ign:ō?$܆ j 5li xm5Xɇ\K BCOÖo{O?L>){5X j;nGEqn& R"@š e֏Qdc`p3nYӭLDcH,?ZP:۱{9.H3Ve&pj^ یdHL FX;#%vnaAA*M]&72f<15WbV}7Ux4ӭ4%a\ٟv~ѰHVK ;_ϊt QWM upd1&r+:E.^՟dRK,*_m0e[h7l&inib®JH 8dp8qpfIfoo:˔w;*(ː<~rncyGfaft ZWGrPr]ZJ226٧ pW)jw<83mɒ><ntb㼄n?-:G6N;Ҽ۬e,$rN{筲?9l2Zͱe0Y6W86]nsrNem,2rlH CS+ VwP2il ~pd>0m:IyaG,>@ʵ_q Ɗ[-fQkewo>LUQ%OmY~fYc%'o> stream x[[oܸ~}IvGa28ra N k))xA J HAh0$RH0RoO86eEdŃmܥqj2]iYllj`q$! ^H ڀP.K%b߅L s8h"]T׍,_x|qw*0, ;6y׎ Ru !%H()``j! Bp6;J@Nn%ĹscX>^(͉e㛛mw{w{Z2h% c;YeܹԹU~的ICP9 dHD${4 y*C<=~(|VjaG@+%m6C\a3j\.&0HA%VVC]װie4\!QFs& Sī̤s5="RbsM$ ; TV٣$&w~iw@Lj2H Bg@^f]eyxۋqr- hH4ۄq'1 ͥ% 7CZ+Nz&,CqƑiGIف늌=efz^cG9v&*֛(7erX76@c_78 hMlAW$"tGl}K)od 2 $Sal iUe dJbN:h/WPդ[Gݙ&Q:n^4 a^cAǼC5"Cd"CDDj `r2H;J1s&gisXb_9&Ӕya4=FgxE-Ƣ.mغ0i.\|s ̅d(:J3ȱ֏(F;U|CQX='vv9u`=j"A o0.֒J\ Ibsցs,B!)Z^㣅~7hg:ɇ'qX0:觃: IpCemϬײ[ӻV*-޺ vf{JUakf5EGݾ?0IZ7YDMD޲rkDS皅}+>S?e|3P1|l5h7C-F%`Y Z9N )1v?"姺jWqbICI/8>Ac0tC-t5U`هfHkQicĀ_ endstream endobj 435 0 obj << /Length 2305 /Filter /FlateDecode >> stream x\[o~`>XpoA i-SZr%c4()0`RKq93'bOW_מFZP<b{R+_n?&wTۿeA<_Elp~#2W"ORG\Ko7=A#99AT 8_x?_ bٳ'ʯ$ iQI"0(+xy @PogҞ 巟&m CQ8ؓƖB#LǕDfKD9#;fs+WWiEѕsI_ ISUR'aDRy >4ã~VzGP#(XeO./Hoރ:SPF'lۧtTy_MƷIeal?;W Zw`Ar8x\M>PզQ ܹKznǽ%7ۢuK4n;ƱvU}et.}g|Gc~x R3FV=2V=r6_ob{ a.H!zL zʮ`˧H)Uŋ k[-GDh5AF.<+5oI,MKxP("ԅ^*|K$>.vy!IO}7 ]Ep|z';qJ:ʫhVkаKc#^HZ?5˃ 2)B:l.AzKU $-챁*A7a4}Yv{[EzYImn9Z*VHZ*歹plkE 䃀b0䰢S%{0+H=bkBYL*m_B[G؞1U$7s|c606|ނ~,99_@ ܝuhۻv7C{j'>L#\nĉvL$C+-%:|NÇK|u g:j<ҦfR[$n=QRa4fo x)bR6P pɂ4fvijs8\G9(bxoVM6I!(;CgⱃF ZdH7D:="!Xf_nRl [TYo<Njس^('+'UԷycŃ\Cvv=)>_b5\'tT)rBjq\6.QŸ,o\4|^aZ6jl&v7Ӫ$  /+\{n=({@upCn6/aV{1k=;Hq7(4*+ b@ӑU~ۃERtBaEvPP[bi5ؠ󩝻rA۹ VY=>JAk+6H:)ch& MJ%d|1B&~Zm1 o,./yYm>czy3 (3c3㘔6M 4eqX/9ٱLfbN%i Q>-7MFQ'*9 &(g䏈Qpoa7b[}Ę;چC棭uF~ j<0-@OQ:}]_ j|F@+h:og+tV.0-ќɏf?55dq27ܵ%;?[\Ӛ/|?oݸ vVɸ9|Teuw7.??<1sݸ[4n#Y8i|Ugbܣi52Ϡ * 1J9ƈO!j~r7/6bIFqʎa1$VCk QKa[J<7(iZ[5qFyRf {rMU10Ù){Rf+kP}ÓϵbARg ~)ϩڐ4k 0N I05=:YC\Ɇ u]eO]0]%)NSΖyɋTᆰL9dw19,7v>;+rEtF[ !Uzߏiq_gcNw4>/\n| ֠iJ|#۟(+eɇOjfD;fh!?]' endstream endobj 440 0 obj << /Length 2467 /Filter /FlateDecode >> stream x[[sH~ϯf ΄{w;WFtqK""PhvnBf2d !̚&hn?Ag(Ip^NGUs>֑z}iw=\.I&^=vȾqͧ oZG7FہǫpsՑּpi0"1q03rrh70@4x `)[1_Q's xPn 0M%.EW~QQEyMJT ^?UUDEP84o: +TtdOso󘂿Rl@-R;"{X+v`2*Jm7r0%lyMOWa-0ȔaHV(1H9 6YJzj ͗`OL(?ZFi*ܚZYօ<  %74G֭!ܔ_Y!H E+tM23(C$e> |eQ "AWj,!3 Qc_^2Gm%NC;ݘ`,,VU&֋q2]d*|rV(AjVDcoR" -93HPʳxRUq0Vh4FAzmqm7Bҷ_8/,I>Γ"zy7xRvvKlM?߼>o.4wÿ-޾L;?~իdw3tap NL e|e~ |7b1LA)Q7fɿN^vvEȹ,^Bdt'ǯ~TfZ\kSŴW'>bI`lw([tc'sT UW/^,Em̹*[e_泚D-K\g(P3D|Qg\WC)l@OVOH6xv{hhOg}獓(@e\o188?-GaG>mpDVSAOGy^k-oܚ/4@(i.J|nb2[u5?.4:YU)[D*&ƝُxVP̒ș.*#6N[S7/"&^둥ex%lqYa~P&};en{M9ՃEYfAl_sQ܂kLho{MplQ_ʖ5^+Q|&ivh84_VN*JcT_B[| rl_=Z7-.2߶ڲcrg83 }_%/ chSԄr>'/Ee|\E9@h0 |x0*0K?pj: O"Ww ƾJ""dP i E 7z'W(FUJ$I^]&fPcR$}A/0UǕOaQǪ <͖YL1ud endstream endobj 444 0 obj << /Length 2491 /Filter /FlateDecode >> stream x]mOHί|4vv1\Bb sqMch_cwL_ QnW=U]U]`/{?N.2R&5ֽC C* 1o?_5Ա^㰭iP$9uwkkA IGFOl)x r .҄q=KEJb3)Eo11~X;éB>Nx5GTN'QKGB>_N Z‹ܖ BK8#>~;Q *~amxJ#e*&7$7 T [F  ʶN.4,PW qFab|x;x}o/}}i4G؅kt7N$҈DLZ$fZBI$Ml/CF,aqKطHmA;hh/%<jDﴇ *bD,Fqʢdr ]^f a9pm?|W@kݩW+(Lc@N Z)K$aP٪&{qvl{p.<\]& KSdY&Yܼp|l2ވGaeS -cK.̷su͈3x}8|a&f,_+J^47g_7"e{}2/!N.UZ#Kjڱ% f OUbOzSט3+|N䐼`u?,XQ H~1ʑ5k֛AK6I IU 2!So2.G+KeUBq̈́+N깕-I0C;pZ~20 L)uzܟ,7Ŝ4gB)d2y:Ng[ҁy0 S܃ *R'ki,]_bgO2_}?dK,i:.jadRH Fd dkVg9iҔZ("805D~h,7nņSN.>ʽяg_0-fl/ߝ5oIczK:1#Md~fFŜ lA,lƁ Zp_Nn*=.<Pr6Y]ʂ./A"sL+NET_F$өi `L1."LfڙNȭ 5u}A|+ Jl-+K]j d-K&)UaLdI3'd7)ѵ8 ~ď3kW [NRZvZdWv:^Lʩ9ߩ^O}`f m;C.#YʄbF͆ΆW#xN7Ƨbv2HaqR y[73՘h4m Mt7_/7_3eVa,ZZ;5_Qb+u\t|:--wYxCv#P>^,#%;-_`c"vZ^ݲvwu}ԷlWT.oJ,.̳{N/{=YRD=ے]vrY}DŽlM՚vYq躣5= ti@\R(.2[qbxk:y-5"s“.6Ǡ?\D_i]ٹ(XD)&n! ZcXr~; ^}^e5T~׫s4/zڗXW# } *{-XN i"FrOM#"zK:, ݂Eܗ T F@Uz;0 *,t@" endstream endobj 450 0 obj << /Length 3682 /Filter /FlateDecode >> stream x\o6b{53|I"@8ҺiaȻˮqjm&{KQ7Cxk"<Hx:z?ǯWd9O*<{j뎒Y}$ loww~a01KQ R|t4oGH.MHpBy6zfH|d Go`H(ùKΉšpЀNJe2t|Vs[Z|Y<.v[>elHA"<#$a"Wi+I)Z&J?8nM*i~T&21#ЂU۱Ssd׭YH×$n'Ah"ӥ 8m^?:]_{r]y " 4M:rF~J{4rLA;/@IDN5jljptNPI>02( >,6wu$xԑґ^}{ϭ:$TmTx6y2!+`T=e~d?<_X'IQۉW]H>Uojg<%_?JvIw/߼=ywNq:;"){H([(}08bPVH?":pR$}-C9sF% @@Ln'늃ҝ= v W?oI@-'3 B}"I/xz luD6dό|b?&qf \x舥puqi?/̖Ҭ !?밁3H ;D3= ͉b?b9bDEIHB%7$fב$Ъ>mPodsĜF-:>ƓV;FhM#$J+1EM׉Y^|9Y.U4zM._•wkkkUOIa\;1ש;yК(bCWݟpw0!/혛\ϓu8X.Oi5{?=#.4-c6.lOGz&d.O0a/_y#*\m E'/oE\2A[QAASދ H\ƯxA]z |Gb!hg*z-6,h=YrSdN&svA[G5yM{~Z_baҠLۗ]HC"a" Azko"G!4؀Dcj7 _MW FaXS&-TR |ؖq㤸OIvfp3M^-[H*)tޜ!,P$Y ڔ3? N$oNȕ6 ]cCX&LUYKB}ozufںd{`g(v, x6&Ac1%v/ L}lD9S&ֲѱ3Y^3BrmT3/d]H&9eU*zfxbƜ{ EÕV{ MKפt1"]H P]uy[^(J!QѬL8[n ڀփsnTib,%7rO8CBySm9?[ՅN>V]^ʧ|l|)Ss,ͼjI9|Xp;V%(X6T81dH(9fRx!c~r'AX}yd١f .OhT!"uĚ"#I(tֱ>YQqNcpf"Mc1-veR-.b0%\=Î@VɖG: ʀY|f@&}/m `𘾍~ٌb5da)5(սar᡼ :yG"5QOjw-e*X{Æ&ѺJ#)% i$ h{Ν!dE2ȼ,-2_r,lM2ڞgt &{'mΰ/x+c™2=vqHj</EDfBCU CH7~iNX%`ۋ/lTyFw>\`ܩY Mꢣ7%;6 ll [:n CIx4eqm+ FvRC v$ϠbMu+f}+Vc1dilpW-MVfZ-u/MDL3Y " @Ǡ1}FYc9VWLExK{ E2$ F$Y fBZL˞[a`~n}$.j )2?-B[5_8wo-dB V4>4KçF㣁@.v/ qCϴĥ͜}9DQy337Na)E"Fwq;teZOm "toFBb:E Эؿ@KeY&3hnݑIr_habVXIb_16~ TjXQ wb9 h0P+Y ?]u0gեkQAP&0C8\P^I{dض*)2P CL("KmR.¨&ac-8*CɄ"qm z C']Ph xl(;A_ē뙚Nzڜe],s숛4qf3f8 N(wmodXH w4 j3i$cޗ1QQ}هcND9ci6e]GU;l[$1D_x7i@CcpE>)Cj Cs/ʬ}kI/*M4yBitIV⓫{I֩4!}")  wwܯ;$t ~pS'GK[ Wִ|lzl6w wLDJ '?@'] "pn.)( i4&LKwaΒ4~> [^o }* v*ܝWiђ{YSf(7Grs\u\ޱ$ `Ÿz݉I`!OwFҊmqOZLd]lIs+8b:%n&]tΠF\AaB(Fvtaˍ~k g̝ri!}/AVz,)JG8 o%"p{^ٞ&I endstream endobj 455 0 obj << /Length 2642 /Filter /FlateDecode >> stream x\mo_ bwCo.z(@X=[J-yw{"j9$'%3ÙwaOg]]#_R]/<&bXx׈i]Ͻw?~Ŕ*=c2ۮ8 (/mpih/(\t{SFHʛ>90b>O=.JB{gؑY~"TbD%3oJmN1 B!Ɣe!9|T`TL@S yL4Zv}n[\p?p(ܷMAV80 &iߕصSfڨ!]q_ IVUcp(5"AhRĔxL#,ʪ I Nvd]QTV.< <@FHb} 5ٮ˻-ד;2_>ydۈRBQL{7D0FU23]Y㥮 µh39$ 8 -Z1ԺNt#k=UN.)RUnJ/(N$ě!uzg /]rqw~G^zJV;/:ɼg~kŞ$',~6vi%|E2HMP ~qLq CZHbr}A\jC1ٱ!=NF 08fe |5fK?K,0ӟD,=9]&GEGN8F?!\hQv)nr 2=no (q"|E/߳HS 6)Ii=-K1-k{t {sl7\ O=q4 *Ҕ ,$҄ }DI%_ Ӏ5}O7:"^Mwt(SaZoh.b_FL(l?Իy5~zZEgf 'Oka,v(G0QfW)\jv.4itd۶yu}Co3knO̎nvI j0>"W&j(DFWb(iJq8WvG|cC0$8D! {RG%W\A\n8x^/wݞbjUYTÝ}Rߤb)P!lB$`ʯWUy\!io/S' 'v߳W]hĴODڿN 8׏vFCKhң!`ʈ鄊w endstream endobj 335 0 obj << /Type /ObjStm /N 100 /First 877 /Length 1805 /Filter /FlateDecode >> stream xZKkGcCoףAI DKbbFjv#4;wvg8 Q?zPHAbق(i5dwPJ sD-4O4fLRDłj,KYTN4an.2; f vj؞&H J]H)%`5IpUI ([؇IſrPLn(jdN`"'L~\ZNtqO^uq¹ͫ1dg0cؗ2aH# jP*NXPv5x 3Z-i0Џarqdj?9d1+d9C65ceX4AJJI8PvC&~(fEqLe R B)J5?Etn*BXK993oW+TKuژ] |Kg>[ 5Wx,3c]'|6jw|6a泡k~pXB#v> Hn:)޲l| ggaK0/xxؾ6ŭo_ HdRGW7/W7<,_<{Vn sX>˛kreuxfuݙxwO֟¹-GIR]\a 0r;,鞲aqnb7O.^,ޮOϩ{qpj)t+411q'ޗa:,o1[,T)FwO`q:6HgY(>,{o2+\o7w9485;"2HJaf5mNSET L ܥ'"_nVW2~% !*#U*8Dlp3[HGa{ ODZ)pOid9ZB,j-2PkZ@<36z3Sd+m@\^_P"RJ80 YoiauKWy0+6rem1ZbIZEFZppIyMVsjK^m^5k=IЮ*Seo{R@ m0r; uW(}!(ԲŘ6 lj 6,u͙̥ U'`|HEo)'u*ac-aXQ$ OT~Ik6S+6zpN0q'XTDK& K\2b 0 (U2R=Hcai6FP*:J+n5Y2<;f_u[o+mȊFf2&{4ь7j̞ph`{~i mm_w}0Ŧy>_,ɳ endstream endobj 460 0 obj << /Length 3704 /Filter /FlateDecode >> stream x[{oߟBq d8ķtHH\EZ\+{]m$߽3R/gpK )r8̓r]FqwGߩ4JYjN/"+"&L&2:]Fg?'&?&6YSWD9_YӃ`ӟ?= qiFtj8Z(f2M+7j)͙G?=.#jCx7:,1Gޕ"L# 7KEm>_:]TZ* =TS^}\G(Ԙ voRsONHW2e$hUyt{9x WYlTdaVrK\Xt9)R`l%p0|(a)7ׯ&߽" F˟7NxRǮ/,3?Ϸ&>f¿6d"},ܰT<?u@c >/ӿM Z0a@dN'0an S5x41GI৫Lp[jbQ7uY*Z2032p62k! ۮǿvG'jhWLV&Ygi$ǬܢJoodzbXy{4} ѰXϳrfuhC?}ΪvjZ0PR> C9}p 9 jϐbh@!A[L tɫ:ojm0lXh |+Ztm꯻rawA/ YOgZ0{ N|fq"(}aGQgg|.'Ed""?D1ĢSU%rcObVNY?PaŕnDyȚS8()G5`$0GpxbioGQ ov ^,;,{``RR4|A\1{J|Ridl bULJa_Ŀ7Nsı'bސr}ѫKgt@7e}%+ș:~9(dko٧XgdRUDo/Sm'MK&,O)v# 4晊JY&VWU^bw WU-}sY\ 6Dj 6~aNt)B <_ز0I],=Fq=WwuU56?\TKE@/\rAT,LU7kٶ7xU0hgnoщ{=EY}1p$<6V#F4XIW,l`ڿ[4f_qfU;+k;cB]<<.kZ'k.yG=9_Pq. MڔSe XH]R(aRܜ"ahcZr'bzE EkzvJTF*s{aaD+jy=yV%#n V>y7m4,Xbjh5H0ԧ#i ) 1}"݀ e1[,}/fs҄ddf9Q3 h1з-O!KO!0,4XY]ndʠ,9 cPBrȹ'{,|ѠNdpc x`mq 1D[N }%R4ꤵNپcr 6x k=i;~QSmOts% D2||nƷ0h3N"k]6xՃiЭBI`NW)Mx@HU:6N|W#cTR?H;XuR]5Q0"v )_O1XT@zz*\ \!:C 1ĺ\O0fSƻdܰ_s=M8{!S;XޱdJѢR%DDD1 i}=(&ؑ秜bX$d yT!T+咶uSҢ)1>=0ꥵq@. TՕL6Ÿ.oR!wӳ]Eg^K5@ӹ>|]\DO dAD&Zu{2isIBۦ:N0%^9DU!C,* ҅&rP+û#`*w9pj z<ޠA.z&%^q|}=>ФFvV` `bbL[DL]XjZ5uM13_cWz/ ۂ`hpӝuŖڿDO;ą _OziZ;csb9ԶWD)H_kԓKt 4$ZtYuR$G@A$*ղKR| .'oӡ}>a6n|W@֭*A/G.\E¹ |6\(\N0$U}ܜA!-pZ4c"r`]˯P0v !Q~{)yz~^*;>O]䇢ޚvZ`L v Q#B;^GRjA6 2F+:༃< PK>ѕ zԶ(] v^vbeYÏ0HU.Rĝg@iqh];ø uɐSXH y:*Q<*/;\ Tܭ#v fv}mNu #$szA Vn2TPhZl:B%m!tlE"V0ZNuBrȌ-BRrp=1%U(bcwZ <)"3 LUO܍]Wp3/O9)&/m& y Cu_VHGl5BrP-ۤ!h/LfO|@ 󒘴$&mak@O0~V\{#zb1e퇕 ]`Uߌ?Szߖ RU$oߒ>QI ەG wV'Lou9|00##b> stream x[mo8_CkvR>@Dwߐ%Q[V]!-19yf8 ~!Y&e՚4-7ݔ^䛪y8#")@ 6/S{3"_.s]]ۦ'K&7sD#L@*?~B_IV$u^T I%PolKW|YYuR,0 2KZEo!e `LiDPDVxP$ݕD`D@F8QS(*q̬B/..&4UbMf/& x}c|\6qΊVdbWqOQR]uU"jdNAC.ď`Z(BT=֞ #uH^J"bS\$Ө}F@(xzCtiΡ#%.+7M2YЛ%6.e+tnmڍ(E66e2קxud\%-x1\N/t!.DD fهWZnգL15]‹x٠!߬u~5lpbl.0jSM>H|Foel,uleEW:ŕۂWi֪c jb() ꅷo[\0o!ŵlǪ])TpvՆV]mP8g>م.VGpv#%tIB^ $w`#Az[7r_bpDxH*զC+>!ܰ5:\yE t\axre5 "`#zڟYDBzCEYxpa;Ykg@ܾA26 N|AZt$X?7 a_Py4P1)md:kDA7X}j%-׆Vw^հS`NVt̏ZW^ g70_ m^L;W+:zcRϠ.A}HX8U3[csLgpFJ4׼#ZB ͆ZKM#7L1iӻٻkON@JoNKdNaBy@ECP棱q/}/0a{#+:v3;q:h6GkrMb䨌6?xP[pϯf`ƙa}43#.e1?7Y?n(DFL0)!γCU"񿗭 endstream endobj 472 0 obj << /Length 3031 /Filter /FlateDecode >> stream xkoFF}8M&8~Hǽ%ERSNPp9;3;數ѳ^ȋHs;9QHD(vg/ Elt^uZ-t\i{É"l'G1XzbDElB%_{(ͬ'#@AH4.Gxu+)$Tu= H -P0v/$܊m#lx12bAjxʀWX^>}j_ͩ @VMfu<]0K(Z&9H9F$g AS$kؐigwDP i/f3oYlaV'i3Gȴa,2( !;7{_Xb1aU#?FOYQNaE=oK5$](]H!f' aޠx8GSvĹW#vW%͠Br޸Y64w $T՗t h{j?'nUF- h>poN8uEWǥvQ%0hINnUB;ƃTWS;R:vΊ y_.ginO1 0ͫtw35+eESOidz;JyZѩ縦Oe{I|0cq. )ؿCR!}D,P'qSM$u %%ƇLsyx)7rB.Nnf0A7Ccp {-{oc it8a gW y,sM跕ldR\ľ{q.,H$z #_;wUac[@Suc7tvɔHzwbX{ޛ2 f,sfX!1zg7z RRݗ㒑z*>)2sfBI tF{j;zYkw]k/WSq$Ix$d:LSF+g u)~ 3T^!6eZLB t6 EL9 (>+ז f9mR\RA]RUWV2|OH1Ѱ(5lg*M,ژi4@M\Ң)LQ<#Ҫ-s\t~uzц8휎Sݑ}Wz|WM@GmJQc\WPܷm\y1E$ uhq|pڤ;GDo+Jc_ct8q<9팏9.#I"푚 "C^{P~ (74_ꭆS'TXo4_ퟤΐiE8+CDqފYKRU-d1'(VBNYH iU jзi%p\CNͷ9N]N/q{ JcJYG7hӠi$u,.ۙ 8oQ[P}8d5U0=x_ExYH?)"IKIKWu9̨x_hLУ'T.wkxaڒPΞ]Xx) Iwj#hbkqAs?鴸m We\ǓͿBr(Mu~0*Y OQs5R8Q1 *'t%W&Ffw$?nPCG2cP/%g ;^\iboz:^3w JAcѾߣ1xrB4kĻjGcJ_"NٳA?qѩK$(dw4yD 3PoFIH;oo3Ѓ'jEisϾYԯ לώ@B?V i`2`~c29 endstream endobj 476 0 obj << /Length 1998 /Filter /FlateDecode >> stream xZmo6_!d_l`fHEba-VmZ_$K~k-)h$LJN"9ӁFZPLn(>[#wa8};ヸK@uln{H`i#wbyuit} 6ڜ[ߜ[6ZeAOϛ^-QX.]]77f!i*5"{`v~8RȘ)=#f#iּKB8z@ , 3AఓC:S:$A2$={[tIk#nZ/E,Ma_n-L V] 겚O_ r~X}U˫jqc|O6;eUJhWЕ=!զF=r,!A5&ZtPǂ![CԣݩUN *i=:>Cˎ $)?̓ =@6L} ?87ȧ/5ϫsg}3>%nׂCϱEGt C6V{*@KR)+a=Ʌk濨0m$65"ͦWs#'&IjMnL7 OH Fq,ֱ"Q.X!%+`R"<ֽ,(daQ`D*+(vCvQAG |PN(+8X0JV8=,\9~עaDZբY?h@kF-mԹ#Bza2ե$,A  "ҕ =%g1SErJ GĪkm8])袣1du! e##R8tiJؐ|ӟ!\ iHL޿@"Tt endstream endobj 481 0 obj << /Length 2811 /Filter /FlateDecode >> stream x[[s۸~`>3 K'ٝm6/&t-RTOs%Rv<@ ·<ǫ~$x]z!8""<.<&gەʤL쵩*(I/W]zƠ1ӢOdz_7%"{%#< >Z )/`c^)%C"DhQ-^SI")蟯Uqھ|Ȗ7A$-ʧ|t6SA!b!_0:IT6KgĨU+ .'P^^|Y'/]]~k&&TmJ/&ٺ߂"c$ҨX$/"ϖUiL;B\BWB͟AG 6GKXJΐ}%62T.RP]09KVdlε.߇!)A-2 n.g E1 z(3dV')۲^2 ,0oe2&V? A<Sq" eTaXCr)bHANi@oGM@{=7U*s?_F SQt+99 K+g_M‡K,J 紑X%ŗ\V ǜӮl ×AG,YG7 9 \'bX׀G;;.BQ21\C,$e`p:tGg5`B|/窰!j}hqAB KfUe=K*+ $U.JuLԨSݦwjƪqamS\ቆ:) K@NU P ̵L PB+E!e i40/HmW7z`i6ޫXϭhr,< q2Goȥ|'v>!ȲZcxm$][JIXG(95J38|zD@% eL(c~Lx\|:vt=+ ^(vώ,Ү0wJ2ݧU4/ lѰ(DUC~gD6髚-rq w}N^&%< d# yDDIA37y#G\hw-ALb/ΒxcuF(DsdmF'/qK NxHҬ'`>sr/N.bQץ ]0",[4mZf۠iCd郄A0Zrf޾i8?.yR|*!%yIB)8 }%yYlӑ> xGb}mMGB@ѝ:5-圔v̉aJ>fbɮ}4hx8x~XM\ryc)4uBDF{R3')MB g1ϙ7E?GG 1Åo[4!HY׃gA¸63ҧ9H3%]>tq2DY!Z'>r׵>{HaxFv(2:xq | \찯OWgB{'>D!yonϐKXCrMr;S3Dbo]K@r@G+4ꂡ3QhC.kZQ: yI=oQrvDr4ڤ^Ϛ nqb=wpF$tT6'!z[T~"XZ! ?iLsNl,Ҕn3}>07x2> K 3APӑג,қ N'Ral7a2yQxPϩS{ZYj(9 ڏkkW0%<Į0 |=yXto :YA"-LL< ڎ6R <]T0k &/ySiΩC!fcP8J,vqŴh{Z;ɺ-'ukB+P*R54ݐ}u ٽ `IlYҾrj VϖID_5R,7I&7K=ޡjWw Ԧ{Ċͫ8FHQ5 t*LDH-]wij-nA [:*XkZC1wb~V>sqV,bf{lZ^9 c~ofsM՜glh %&2/Y`C'fЈHM)&Hy{ڜC3Lꈜm&_ s*[MZ?],;I(h)=wj`ˍZ^CvNơ n҅h}amN % h#Oh~v׾L%7)$yMүJ}ƒ<&97ݗBhf"1 $/ DqR $b S5tl+nUel/>QW>{/* Od] @s+fI6ne[4Y:?HHs endstream endobj 485 0 obj << /Length 2309 /Filter /FlateDecode >> stream x[moF_U}.IuZ9qŋDH*._EI%MV33;F,~O( P/fIVSS2~pcssbt4 g(n$`ֹ Q$ڛӛr( @AI#I̠z-@>7W}t91Q/fײou4/G1ZG`R3Ѕtr4H V@av,vTʼn1{'WG#X։; ,7]%,Ll\O,Knm] %U5#!Y鼰|t %]+ ]Hn"Y5q48$~KaGZ=۹#o6[h+R_Boap#wTn^#.bW!GK0Q%M@Fcr$c "RAM*@&Zq&@dɕ@qۯ[{^Ę$g*΍1n6RnmWvtB |J4/߾^rooL 2 bq8͠Ӄ$x8b61 q횰Mg;5V,zX*FB^Ln<e9m\"w::?XmQcPD'JTfL۲ynj(5fY{ͫpZ2 Uʿva~>ϯs{+dqeRQa-„)׬XٌY-3rU|i4ڍ֑> stream x[mo_* fwY89$A:ΡE.0hHE{g_H%[҃k\<3;;U^<} D28  T,8o޿|u4J 8-"cwOh/ D>9xuvp@܌P8A#ڎ\D$xw)y~KB)EH2 fix ̳ܿk$[}Ot+I`5:{aBA=)L? Q$"Dy\ӫs(Ԅfa!xDz^F]pz'-OXk YpĹ;͓2Zvxq-S),G`VąkEy48E%ޒpBo mS`g0oR<?Kegicv DqQ lGVc's/((EQm&yp8ǿwGXn{twb}'~Kc˞ӊWӳj)@WTI|emQ@p/"~0>9CM, K+;.ۥ au7|Ye"mZm(AG{BڱQdX;f)M5m95Hs%`^/|^JQ-C>dupoׂČo$_lɍCDI{C}mϺx]w?z~'lCaƓer"OqQ9 )Ғ|,3Fɨs2k~0Z̡p[qUً}p QK@~}"&& !Q XĸgtD'!\wΓo;@!蠉PW(deGט0kg{+A>%B+Zi\9$XDzPXVՉJv0IU +R'l%Mg. t# ,t{mTߊZa6II1d;:)IҊ8׎.yU(mo[;MKU0 M#!ƟLxebyjOقkkUFu˶ʃgzq7nZBbjP)uP5 ʻ ,GhɗEHN$%`S05pP5;7CeNvDJ?1cnB^WY$8bU Iye0G25°tİa9Uʺd8v pcYwϐ~HjDc>ɯӡPǠIZem>fi'BdqjJyE[̿ s &reWC 5ǭ.ȂW O%Js $l y;p.rΐpbHq/~:(ӬrJ\,2]&DP V}2[Fj DO&?m+m!{%LI:[vf:M2EDO'x S3~Z\[8;/2݃Qsx蹭v;#2w{~փ3SJa6/@{uu }ꖓ-.w}Jxq{ٮ7K6N.@zijm3v$[=W!h^N>zJxm3*FyH{[oj[Y77 xmQJ( @KWz*{qTEW璜PenOz67vk]K ;5ܢnJlN<,MKػznb$W" ᗝѴ7PTy~J/|LZi3fn^ߪNbgf]gwͭ;kՇI*e㞫ǝu"V6M?T`p Y +W*Q u+gR;"5xۃObO^֣mصÎ=Z35_d:#x"@<Q72lT"32DJ5U_ ftPrג:cUg_nBC"X*(XY' (Y՟|kBrˏ=9Z&[Lx3jѬch )?(kxZ,,OIC!Lw endstream endobj 493 0 obj << /Length 2465 /Filter /FlateDecode >> stream x\YoH~(y1p2ٝ zÐ%֮DM o5Iݔ(I)v0`-U}_O~<9MdTFw 1,"e4bE9Pxtd|}qwgP$9x'9FE501ˆ}IkqAUѿNys2q>.>&e@(L,RH!g,po2NyBnM'qRc0} d|'bý4`i!8 Q"B% iŹ|f920ˬK).9gdܑ%SRVq g\v$?Tٟ\0YM|a P1OF35KGg+dk|.F)vYL9w[\Z학aPENQxHɗʗD|p2dGްY;nfqL17WH SVmw6e)qO-{")"W{1*Ǐ?O!p]J`6!?+[5WeֽY7㛻0FO᏾qa6(ı2arODQr6Ws 麸ujhykA KŽIvZo)?^omvNP*%lB08PXk D@t8!FX,#  b3$KE~g`8Oa40n~7gkw4\սDxy~l8.¨1US;.?7IFPM#eaۘ)~:#kEW.^qw1k^'䱛<`vSv37zgb5ao2#p2O0AEa 6d+7<Pd7:fI];*T`z!Ue]g] v*O" C*ƈ6c|agǐDnR&4`Jio$M׉}ȍcm|yD .F>-ZK+,9m5'J>OWb3iIJ7epIda_Ht/rV9[E gbky!Wu|zGY Ƌx)dct @оVȆs'AʰBTb0Y "!X*.ܛ5.7 i[̔C(?VF0,f}>vG0"/ٝW_R0iQ+cB> stream x[oF_3A 5EлK]!- ZYRJRu7;H#-rW,{7}}˷*b"V^(0wϾዯ."f+Mdeޙ$qi/~쫫_8{ͨC/ݜ@#xJs&ڹ?|з5}ݽ YyWB)4P|ed,QVUqme.(ɍ!NŹA|J4jJLh鉐E*@éƲ*G\0 UBA*4 Rz|j/N*4>n~t>9Lv}wvɨ$9t.Y$Cf3-ί{?|4[S,Ilkw 10hWFnLTEELz7 <~Qn(#/R-]'Q,L%/NTnjXo!gV4& Y;P[V??01.{#qw$u."` F~&D/=#.*L(8bqH~bwj ] B|f=^pCϚI̩Had^BEaIA`׳s+tiyAj 8MFof*BA[ ]ȱRG7-7 yÇ$]iQB~F5`" E}({`m#)An% UmzB{ 6ZRfߚ4CU+tȻUf[gd_] r] 3R%ʛ.FQ0Mr. bϋ%do]ޕ+GH zv$MQXq#eU|sͮ&(7ńh@3 @ILa++p2C}%MSe vd-%2&;DȰd$p,24N-]^&Kk.DMXAJ5 .lHXhSL];]3l\ wyLRGTMLH p8q!uRv v5(5 ,,7/R]r<O2 j#;m@ fc;D*JB81}_{CNFY'30y%LL\R5۪k͡7իgN}532x<=46P bJQ&w9WiE!x^=% <_hdac_[u|e! ݏɢOжV6?~ `US)rnմ>BNrND@B%`S|1u˗nuٶ̎m!MPwXϧg߬wrFN3Z= xٿw-^ G`g0'<,'D'&y^8&0>dyȉlq v^PӡEzO "N=zSwQa4@D:LiaM|3ko%FP|sahk`#Y8 rbo^`Y"Z piכri5W\Lr=OBC17՝ '`u>6hC4+ f"-\(|.POr+n;3[8Giw=_R揎pgiִGFryǞIn85 O5+ʈGO7 0H2Tһ47Ydϒ&E24S hEÆ [hFyYn1W%5f͌<\˸sGL:M䮞TW])~Fz#Ld* ˑ鐐JvN#л$%) tq iϮ.B0\@`vHeF>"p1,,)JVdu;\"ު/iBVrI6z?Y ]I{)Lݳ*wԏJz>|$?nCCnxׯ£UT}mr6ΣUI~GZn-l!xHJlRr| `hJ 1vZ[ 9C-nQk+ 48v>[_$/zvl_cWr# J1c>jv蠾RQǽwB <ߖ8ZtPe f 9g#"-7ޕ9M=2')Dq\0OK}~4.]MilX\W.!rq_kR]ǣV'_>ҸL ~Sy~YݗyN:n\KaƂ='G 1k]`콍C5_s'env$珘jWL0H=xC$()i!CG*~6Oŝ.@z]a7jӾṅeIIOJ}B#w(2 ;]MY8r.b @c8?قqȘ 6d?1jC4! dsخr' ) |JM }RǞ!Y4\Bv߱vqwO.}3)'\ D)PNȃeGǸX3K PpL sP "/ɶIb UKd7'Ejk5e%]>C endstream endobj 502 0 obj << /Length 1746 /Filter /FlateDecode >> stream x\mo6_`5bnM~epc%[#%Y2iE%ՉS#c&y; W^w^wNϸ 22`!EFL|^*9XLik,M.w_@+L+ 9mUi2~R=Q5.pNGI1V5,*`aGݖE7ˢzWLGŢY2z,ʘâvGʢX>eH uA5#5Ԥ}P{#I8Y1^\O6ج# 6ӆ⣷.swvח&t՗~W4/=i0hBaM1$@B@ؕ1 *i`BxM"x :LlocLos,ĢOA}qNnZN $h^E!M 0K)2P`Li|t됫-_R,(^NM?R(؉Hyӎ" *M hibŴLC3ֳ /yA{C_rnVDȗӟji5H!`eǘRGMyddH #{H|XY"ƔIxcjvV_!'KVhH.KwB̵>o3FE8%g6) {<^mwLU;FnT-9'ؼoFiy6v8ierܔgJ\<ĕX?iYdډ?a+k44J~xp-ݑQtǐʝBgkDŽfs`Pp7_JYG`){˫  i endstream endobj 507 0 obj << /Length 2572 /Filter /FlateDecode >> stream x[YsF~ׯ@T~ќ8RqRNvX9K\=3(GɖD`iuOCzN~ U5 C#;/ @ 5\1fO&}*;7G/|^zkH pW{! }Ck]ZiI(i45 P8>`K}F*8g@ݕ+w[.2[G)p&x<)Fځ_v|X̜9J sd54]NWTn~C9RSjKh4Zwu{q0sII=1#Xb{5ՓI6LNG7uV\=(d8zdC}#SM9DAr&6d3+ېHk?DF9[p<1Oz<3׵:BHRk#S7,Nn]fHu &2K這<6E %90tt)XbQ˚ñZ6-Y0:Fgy&e ¿Ϧ]m芽/G%FFBLcG [15CSU}*GzjۍuKs9$t`xalSD߾r~wzۨ^z纄tv 6zloq3,J L(5dG O]pP\wDIXVJXKN°Wl̼#j\|0urM6UIUu@yV]L`o[x֩n&xjr"-ƈjWXWGYnYfq$7\k׸u9d'(I;kH\` qgB/tyۗj|CE` }`)_#Su!dltwU8wu.QYoBOAfAhegfsрfx]FxQ+^COU מx7ynښ5&,£:FD`ÝSc>&MH_ yu< 1$՘e+x#P(($+#&}v4AQi.ub Pki"sW_-rDOќ3bX_FǮ7O|6VG罢[}벼M^ZD%jN= T4Qaw?$A/ v;O6;Ôș"Q\wOfNjT\݆x p=k]~u 9r>a"zXc75_d߿w> xGpcvED x;M^ P{MSno@:`iZ% vpV?2ltavGղ*</fa endstream endobj 511 0 obj << /Length 2220 /Filter /FlateDecode >> stream x[[۸}_!l2 m.6o}JCcsjmեAޏ"e[T/ 0(<G?_zO%>b!EE_P^LΊ:}H r۫]hG$RjH$*,>}?D$ۯQ%!=~ ;}_>D6՛V pQ01pJ29#a"S*,6,5gESAC+v$2c92ABBF& `OPLXTdޮ*| )D5K@IQ :DzN/NB+rC0JDc7jn*$Fpl AcA]`> w\n:s!;wBPq8wAXv Chx@M2? iYlj1kcfa!QIgeˌzJs4dCbg B&a&h!qr<] @I I:X6l}oheM:^@'<ML.8]ĖeI(("8GI0c$uKgKe/K4e ucUUū}8x:WC!ceZBP**Ff@țV Q*`}`% D$*n촶51U2.}p,ːe kX;1C9dgo+C9zqi!8]kdC_yCc }QLØ؛}+-7iZU^ŃZzy6a9Cxϟ1SDQ̔f`&0@9>LL&o\z-?{9Y "TWnj =y{U=5JPB!tAY`H%h&  ^M)<3c9?}etz Vt[jZ4'O6 ׫ɠ֪fz>BPO_0 s^֮ȧup|[~O0LU.eJ|6ΈˈwM4NtThfl}tC1g36}..isq^תO9t R|z+oD-T%:KH( [I#HKx3ΛlŲ麨y A(dx?:e}3ؙCz> Xd͂/Ɔ!uھx˗Fi,9+ʢzuP +6Q>MHXŒ51)"?@x|Z>K7G:N.IW<^#`[= fo ąH8]1nNT )w' NI_ߐDYVk"F\.]a; -@Y"o_Q\j՘zs_t2Ys ׄr{S:ZlUU6 ӓԍY P=|SXC%k6k QBTo12ۂ\Z߻N&Kn,l},w;OmWC-7c9mɞ !Tcp+ݵL'Htگ|ftdomir͝6񘙝VMv(/E0S5>4U{-TW,ZsfӦ+T_=͡b> 1B=W f.T^?Ϫ2QϠN 1uqjj60cDce̡ɵ*@3]j,Ϳt@ko0S湒w=)btH)f)[!M.h68wak2qZ?Hbٟ,O{YUg8Fؠ_Ro70O;Zf&:mrk #|^K<ǧq!WZ>-<;cCo 7a2KA%C5`tu4 N-)xKm1,0IT#`co endstream endobj 515 0 obj << /Length 2365 /Filter /FlateDecode >> stream x՛s6W$T0?nvڻ]>:Ze^$Ǐ ErB}Ƀ)\.?,GGtyv #.o"E#e4bEo?<_Pg̗6ɪJ;_1$q D_~8gހ#%r$۳Oh"ѽ * כ׳} i('U^*K>"1[,R.T/o|auT+m7:\|ͭa>[s*f_컓W%wM7O.ީF0 =Bms]dU/UQ'rL[7mڐd&Yyт тd9:"dvsg_Ҽ.y]nڻIfiNWueErv.oLþUEZT٧㢹] rm϶ͭiDvm.J:m_ ݃& X6iAᢴUMwqDd%rxcl_uJ`IgPH0!1\(D81ItcPV;:‘фYv@aa਩yXp GjFa Ag[ͿLCGŽH)0F3V IRXvT;4z28q:cjӑuD z2DBq vbxW^(  ރ!f4(V"Ӌ3ZpxjB }.xD<4/hE0/8Z ^P36<OȦ\JtΠqh-&Syg4< V7R9hEpCP ΎP3MEM3 BmZ3Ì_ df¯W D΂)Gk12Jg2* ?[1 AȀłrFk12cӯ2:):0N0AV9Z ak0D $fbF *hϊ08 ZL D77Hq 3SP r2VAX%‰Z_`f0 ͱ`9c{,2nzJA !R K tFX ЈjbF8@v)Jף،zO)!> xCi!$[{`i6+}t2qj\ئsABҝ*퉷w@P:  #X^,] Ij}IiNcf8TCHD?,i45>XrBLMLxpho{Pɟ<c7%Zü锸v/vv$E =9%2_|{廅>C/43*d}"_'Dƒ5fjd_CnsOm*Zb!0 "s7@OxԱ`Giv}qUhP`hziRS^\}> stream x[moF_3t6ʗK6Cqs?%AKZ"]7|Y,R1r"&$ggfggwQ陸/N^wq % Px 8 7$+2ͳ33KLub.8QM?\=Py(櫓w%" ʓpsj٤~q㙓_~h|`De(8SpxOYfd*:.9/)WiYۑ&Wۓ3s;HG28g* \ /`$r]۔JL@@)'gB9薁*Tywz^ K7$`&MEn+@tF A"Q,^I|wy. 맋_?ϗPU3 g5߼Z]^uqLɌQȼ~+Uv{~G/9 uf~kMo'=tw JZ(+;ʆrG-J*.Ƽ2AD;'Fˇ$dw9Z| D#H>(,>kCBUj(  T a fVo(q!%`ms Z\\Ez5t)H5\9}3}0v\= " k>*vM3;93gͲL2]%k2 THKChS>LٙX vo !JM6k͓-ޜdi+e )T:6(Cڋ)SM%"A[sdym.ӊ|?LaԎ WFfY&Ojb,V|SoJs`'ѿ+P.XQB0+vzK:$ `hyߓB?e6wV].s LPq4|E(rӘ"^[4ei:9[Qw6ʋ gK57E)Cb&QZjl.MrݡW|H)lXҁvcAc>3Q;9$L!}D"|xf `߿WGL^s,lH"-mHOeTZ} vlFk0kQZ\LY8UF ^PSA 4|L2m89bo]52Fe(vx0tb%ҳUb7}zm8Y܆Ue.mܖhY#N騍zX7钑^ȧղ 0ؐ F|w1 !q JKBa*]g6wFL3, 7`Ci1)p2BOdofw@g.Zt,X NΟqT[vBaU-s: Dqy;K I]NErHe̊lkG avfq¦8;ORSV=u P>H GE;[]C ;5Ok]-Pkkqc`U~:gVu/6"YAȃkg<ͰθuYUpzD#O; i135LsFFw!>gu 40?sug7 ` pnvFzSIeT#.O6/~vx /\L#qA 5Ks`̅"3:|r|0"vi>x uw;S%;^F)C} m_wbUlՆEv%8QhD"[ ,vTkp+UsNz\ aӃ?zRg%\/l'TǤ%xiBlFՑ㮄}/#ӧ@U5qٍBG;]T?X!ϸol [->*k}p>ʙY=>Y;EGͳHBŷ:V}"b#uXJ4U~}\RܚgBzYgkܷ2c aIel,{6 2% ISC6H?Bφm sqj"2`(G6]_gH8] 3oX'7oXV;b_V)XjWM``Ѿ7H}gTD у Gj1.U]14ZJ>\\ǹϟ^Bo~3n=wr_x+Id$'>'әC6cFڻ endstream endobj 524 0 obj << /Length 3315 /Filter /FlateDecode >> stream x\YoF~ތ웜'Ʌ ;q2AKTxc,oUw4}IZ!1fꫳ k/~8zs~tD)QHxȽ/o;1zdU\yҶOVI\&#Г<# 3# "#Gx h BZ{BR´ѿGa`מyCЮI ƈM@<ҬN@ "_ۻM0Jt-uX>NN=Zdˀ{ED B5S$|w1D!ȫ$PBY^HDHw}Oס|03͜5HBeβי*Byt~l_g*@L*`Lprh@"9(X"!cFxӌfd40\AVL̗=CO(z5-r/wxGC-qjܵ ?Pb~6\ALb9w>bCLAdx/?)wa"Cz4:8d ! R{뗇~6iknE9mV烕 `9i2.>;wJ13t!8n!U< b QoF Ci ]RGIЮՃBDnI%*ΰhm+[M ܙ"8e C4Ԡ\BP s# a"FC O!"zXj؛JxYЏ/slΛ8+ _ TIaTaTeoI۲JT 5vIW+;*n5/SC{97c|/d3A1 RX; DWduk3]FhMձBNfBHL(@שm3'4Mw8J5-Rm|^"ƈ v0C⢲=sip,-eΗ& \"-"2"Npg4 Kc`M^TqVٷ|usA$mA>ϼ[Z{xZ=z k7X˸HgUQ'_MS liv[ͮQ:%|H4Sf"ΰQyao놛y^ol'2! 6(A m xbtl&h #ByD@5 U5Ψa];]2\.F}_ta.*xZC e75WPi+4%1V0|iEĭ1.1{. Zrrj NcgÆyG@HrNKrp3'6W͋+lN&Ձnx>sKLv ؏5r0S  ]ˈx .p&rFg_`Syki ĎVeҁSIw$)iD` 5v*  w ;~+4=7aAZqn] "cK秾ei2lZ6.Jt+09ehEP~63, ;_hp)#gP1R,_v3($!'|sxRfkv^ G+Q1ˆMKQ[7HΊ*f-g>gއ<{"@oYwmXl92rbs]6 r6=FNwo%mvt"qc1(WlTC]]аgaʮ,'U^ڝI婥8GÕb-Űeݢ%\gG2o  g[m leHF+r;" *E?[cHaAC&$-4U[T{mW\>s x𕂎\5.௶d ]UϩcaH8UECHezjO8wդը$ù;sHԙFYo9E=ol91du;KږOC :}^n"]M# s¡V{Ӿ>2"')ؚBAH$f J(%ۻ#,_}=YigҏaߛfP'ښǃYfpSUr1fPEhfr9v6ϳ37AAp|bw0Jd1;a>h}۸[}_F$hsG/σ潅 '}N',^ݖ{H9¯ $RaɂeP[h&JF7Zz7DmO嶥60;Qq# tbC@Vt3߅hB.1wn?٢H/ǵRX5?`~`,Mev=us1 {~ٽ:xZUn9:-ʯ endstream endobj 528 0 obj << /Length 2168 /Filter /FlateDecode >> stream x\[o6~ϯP1ëH}()0 t>u@X;rb{%ZiٖEIs{~=>{  by7sɇ_rJ|ζ(^F -eknM>Oy_yJk-e)FX_x(:L %[ EN`޲ex,6e>Dp.Z/}+r@ 9HI@&vypLH4073 ­m h+Pq'iY)`c{^?e;!雁K\2S@.O@(g,M@X:K; !l, (Fj28QZ,4Q2pV,N謚Bo= ͶI"]FY44%FQH[ΛJob{ ;Mt(>,KRt7ja[c+{sm=Vt/gw#jTt~ gs~ }rExrk7fP0 $g@ގfמpQM'3rt/ֈ 9&!YuYF\gߗG@@ F:yQ{]/ѱvbأSS&l!lR&Mtc1v0[-b|s5lBCpb'0EE:}"y nZJ[w?, ҂l#5 dN4ll/kXӢYs 6>׃A:6m(}qZҴݫq{"طCca?ut07bb)n( nQGv;s.z{[*w|۹?mzw0W~K'%ts%=,]m&ܻ%xDmZm/5@Fk~VLv/ b%66X~vzG"n` 9y'G)$u-kG}^RsQ2 CwN/Q &+8}1^.;3Lg%Uӿ}\֏sC}}g]x9Yk&YlPN+drgr,U |Ӱ('HjQNX<Q/n\ij{d@_A¤mv#W-8\dI\UuOXa͔ AoN]3CD"N>IG8޷p ' 5 endstream endobj 534 0 obj << /Length 3058 /Filter /FlateDecode >> stream x[o6_˓}~a^ohqmSC[ĺȒ+ɻ ?fHrl)(7#-/&n` Fg݃PÉarRi•lNxr[i_5$4L؀Rr$FNtVo qDJ<]qgmŹ_:g*t?I~,)q%YFQkzϏ̻ϗkI-"3HChwhCjy20}ȞHP*nҮbTcݠcWv$0*zf4 gNoʹZn3f68l 𰦖E- !א*`S"tcxI؎2ploGxȻԯZqTK79VIj \r1~9tkxOkT9D;$+(M-/p똅"*_e}ܨ-27*q5y5IPo^Gg$M*i.S)QIj^1UB@5Ǧoߺ8VWq mMfvڅn B￾A&€h.D>> 3㿾wGeh& FAѡ ]h¤$4 #! 'Ct.)xVaGZsNһrҮc6KOu`"oLjQcٓ+p拹k@ A@z*ul.:'brIc70jt#Hz"sn^ژo~h;F 2AmPta<ׁu{MlPYԫzm8P8 ,KX" {rwnhuL)O-2A}0=T)و;>͘ya(H-2On((@1$YCr#0<0 ӎ1 o˙S(qE(aQLmQ w^<:OI/xr#Epn,V#6io{m $ض(ApT$dԣCzSp&&28 Ox8V:$Ͼ|4+Zw z,Z+ +0 fQmu>uRA=ÀmvkjwDIo Dkh5*jfk7Ǹvvǰ*_=,w|[z^^ ghR aDdٴk˶#bPW-4;s{(>!MoN&;Qt6gf[bKc^jbUG6eLv'ɏ3< v6_ܻ'ΡOb9+*Z+7pif 7(01EyX H8H6 6uu]"Eg6*~g`(]S/Nte˨.m*"#a?"vN#dQ[@ XLfF; |klhyދek?v0v,"ErɌAq@. ^YƞY.ukm-KPk*VnbkP]+)]8Nl2%Im)ݩg*0WU*=?Vd,ܰgwc`q8 YzܷY"; U/njvٲuf%^|[N2Yv֛MH0]u^(m=XO% L7e۝NǪhݤɿrPP͈&)Q,0F!Qo;5!7_,%ٗH/"9_^xan.z" Ȑٝq||Fw?&rkc[ƭq6~xf>R`L=_L~x< hXȷzMm{y2 =gA ՑÊ%æquhE{<Cg j}Y|8Nu >j³ %f|&AvůĕzKCd~sb͔<7Պgu}c9"0!9R̃kјֲܱY.&ٶu5hi{Z WҾk: 9a#8P׫hik5|nWQp/M̴VyDiƀu~a endstream endobj 538 0 obj << /Length 2693 /Filter /FlateDecode >> stream xks۸sւxfnz6ԫSmHx2߻x/Q/RqܴqF@bX.!/g?ݜ]]0Q( n&bX*ԈiL7Ř*=s6Y㴈$K/]ۯ,B@͛o}F`@QȑU0}) 0b>ڷQ%~gأ!p7R>. &fRLH&3wXIIdۧ9Ҷi*E9o+9ȫ6Zg6CpGxP^=k[|}g{hKQbK=ԭzy$]Kqz޵0)&G*qOk ʅc_t*M'{"/B-1g$D;ضAXxT-e4=zb;i^ Vn7Bp{ *=2[lF~P NBLB5iޫ?w2݊~mU/'qS/d'2դ>/R`LN4!n8Qf1;x/bX]c~ɵ2vZ#ڝ;t;WN ;9>-)4:cջKB ԩBs[=fbL%D|(r폖Sȓݻ~l|h+>@n½N~(1ݍJ}W{S*%ǃU/u ēuPSTP|ڐND _Y$̵:j&Y-涓z5 uVO(_zBHPQp@T%I sϕz) $W+Gb_{veOuG:tu6!(.qK2iM,p)Bd;r|>`-Q8OQI8$MH0d k9FYe`.H_ b'(GT ?aVxt_@}̈xYzԖ endstream endobj 542 0 obj << /Length 2477 /Filter /FlateDecode >> stream x[oBXuIwa[駽Cr-{h? EQÙoG~{G1%"R4RFLn}]P')fuIOWiR"Ow﮾pDEDO8C# * U^a!5оK)+L RLH.#XYYMEY]kSTL>fŶ=c 89v''H/c bF `I2 .PXbMV%HQ/+m)6a^Hò 4 ysIdiY.'7Q,bχ0a<wUn|IEQ3ЕU@`i Dzm)@XEQ)z }0#O%qbcu6-je[ Ĉޯ5h ԦϤyq~$Ht,o$Ɠv}4U7 Z|.1^}8P<{ɱ\Qd{on|\'Ol= "QzLcXc0i ǡ/EbF%;|'F$(MAڛhEP0\ A# rT~L]:m?bLWov HJ*%u>'>K3$u8s J8"=gGX769` \Ak!v줃pmns߮}diV7@ smI>8]f c6@Bv g. a.0K|,QI?ާ {&@Ⱦ^BL'>e\2~~9!_$R;@Mvc@ݦxL;=ZT9nu}OPaY:^X9_st@:o1#+;5|_|xQ'$5zt~:!oؑM|u|$՛wOr|i''`gX7Q"[Qٶ*ilUdN@" `8 pnա1r O. ~!5ZX S J['@Atk/I>!DYk͡RHHO.!fň`U*G\"h^y 8YEp|p5 R4Sp*п.( _ݰcK)Z y~0G2z%eQ\ Ywh}7'm\3n1sE}OqJg[ϼ)_$EY7w65>.o>dL> stream x[moF_A fߗ[4;\ѦC#ԉR}B~$ERm~S+0>wa#]{D=))M?`o =ޕ]5 * ףgG^ri%مǕ@x{gc]N8b\R^!7 i]ytrV+$)k_iC $dlӓ_ޝ|SP?'tl۳W+[1oC{ؘ<$ aw%39"U> {o y0 ܱ}98rVxRҩaGlO^Ȥ_ZK5p#vw/NEeT"A5d 8twp{9"q`2ITmOVnQdi(Q !QH38鷙b͏9gB<3n8S1q&D .1s3υBT0*kC {ԆF1c3\8 M@'`Rr"+z> أ%^95DЈ|b |gEYКP|_*ۈ 7RsZ=+4 B?({?&wǃ'@<]F[c9pم! gHpy .|EX7I&Lݞ:PaK4 -ME`fGLz+3%ֽ!H 0ܝPB񥭯P9_uRWW.u6i69-$TQ<owj{jyl'vξ0tMTз:ك7kx,Co["em%By~N%B6%M$Zț" 5Y.c&tnfLD  qd5ԫc>V96Д3R-nR4 mIaA1Q֥4ufcg!T[cWuο>ɑꎌBMICo`,3{cirR yb iph- QOP&wF>*3ԊjalUT[45+6lDY}Y_Ƿ.ChbӺ2ޞdu=@A.5p w* g̫8]8]s(|.rΣydn t7? &i +ż^@"ɾoLI828]CDBgޭ̑kT!6c}ٶFd+J_5ow?`OfE!"> rlo]וֶ*u|zfKX: >{gytGT칽s鶒")NU J#Az%pE!#7~:7{#U3a6 h̛v| T.`/zx}3l! `*3W2N*u^Kd)u[=ҁD^Jߥ&; < iM;x04Cv25.̂#*bւ@B2FK\ endstream endobj 553 0 obj << /Length 2285 /Filter /FlateDecode >> stream x[o_ z wмχ>V6kT(*)T$|f%8듋w< "I*Yh?|6*#:-"s7w:^i@@O>QHD*,N>|( EATIσN~=Clo7z.EP01sJ23#a`'YZ$ZOQ<[2gT >'zg qrloNf`LCz!Ln4`P "*u0iUj@?Lc- aR?!wk3W^yJ!^rpAкHkG! X" !u;д'}ʪ!SHX^{sc*%̜p?/sQ=O4\ȧ؛9K j8zLgզ5k=ԧ,n€hѦ s>3Y~7Mrʝ,r7g%v m@h QGo$daP SxޢpOE^?z.˪K7*w'q',_s"p8`tkl?UHjh@O,(yדbko/$N`٘fl5 j`+ 46XUʤ0(1T*#cmƹBanE(X=IFڳ#xFow *PԱukezM)Mn:ު,'ۘwY {Yؚ:.{Z;0!>rh"nI g/vUAŃ]r?cp}YHĀĎKXэgxq~gM2H,aUx(=mMʤa+Pr^s'x<8S2s )$ v71ַoz72s/<qq;*2C/i:.b_QBJWxEƾ`6Y圻(bjjAHkuXSϿȘw&(dDhf!ԹbjvdRF*v`;W%* +\[}PX`裐 yu+$X; H>}thbi!K$|>?lEN8nsH*d4X*EW.Ǻ t, Ppl+!_UD6\` UNunejwOs7U5ռ(hT&!0c ak+>>0؂1T&agdIz?ZaE#c:Vx†{!#UXofD`LAd\3O)0I-Ss7t aRkm"!_`HPʸ{+tVO 反H` M+M5޷n^tFer:}{0=p}Z`ɺv\:OnaUGM%"@=(4./޻XAh% ePݹCW  QEU>"p@w]A} (0zUӯe8UʀWٗa)h.9^Nfcf!`u^ox~_nH6ѯs/2p >iPTmEpDAb7\Ah4m4>7R:xOk0pޏ;w~]5N ?XI endstream endobj 558 0 obj << /Length 2578 /Filter /FlateDecode >> stream xZ{o6?B \%Q4@{-ZC-Efl]dɕMC 9ӊwW""of(O9?yD +KN/0R+/wj"mt^uZ_`xÈ$toO~?Q/d@Q^9y-)ƎxBRڙ'~@4$ܲ9<_0 MefIZco,ͯaW=-vպA9s*g̩?+4_eLɗ8^"u\ΩtOqsyfUZ*u6A]G\.fM5(iA)DmeJ83@qo_N03,kDR QuR-)YUޕy[~:[R'68ƮrPIȋrg؎wu7m7Nf['N]UnfH,VKXW(LWUXH"|@E}7sAr#s0&#WE UbK7ɭZ}}Ao,>2%3A !1 z޸|?T`u} [XȨgP3;e(@MrU)Du4QjhDžI@u$[?㋈ "N#1[;ziHem-Sw;zsW7%_GB}0F@'(j:\z*˶'L^A`*} QUYqbϛZEb։p-0N, hx E`]X@L ܬ5R|Բ9 Rbc !#hWm>H~dXi4K{LyOOLG|2#'rJ2ݺMݽ9*GL xK"% ]J8j6ZqrO(¡![LҦ.6sSlu]j}DWS[$=PЂ۫ o;P], #9M<c>1/ST)wIe<|mBf#U /h|XwǩS"~W߿%rua!*FɑZǎe CjXJAkbO4"k5|m|&f& qk1EVP/n.s)sZaDxw$|/g"x*?3=J2.u3iT{$T&Hzyo ! +Uя#*P3FۣpS@zk iMQfKr$l{ĚP!h(5$dPyȥΓ&.N ZHh gYO_ЫޒPZ 6Ŝ5V/۴t`XَND6 gum{xQQ]mHCH`i#\YduLdMƽxbYnBwklls6[ BDݫVWjWbotnVIS <ޡ {{$hulP0GÑ=ҟvqkd%zc7##VP{=E =t_SZA,MX.hvYB,p9|O jVźsKq]R DpTT['/jЍN[|4m!wHw+Lp"C3Dx(C^Q^eUON(*cLge!Wo$yu'_pkeD8p/` endstream endobj 562 0 obj << /Length 3150 /Filter /FlateDecode >> stream xo6u?@Sn@znp!P%f[$=%Evj9I׭S$h\4ӗ2 "< ή".7~b<ڌM׋dYe-]dE8Q;G Vs%Q7( Y@*F=~:z ϯx]=bqUI)C%DcOH.Ti,:D]µVy2j6օx,?qޞÈPD _*Ma"ȓ |TME{Zs)N$RמvJ]$bQeH4<`xC5`0N=q3@MD"a7hg-wNvfR:Jll[_ϒ<;/|> P_z>u!1BW?q(LGZl}}j| }qQ &yr~SH tU:i}5RnLL':@ADx-$a0ѵـkA4(XãDh CKbH֧/oc ha40M⃪`Me6٥-s-g>m Xp޴<-!C`!/evYCQ  !}@tq+MD{~3b 6`_ؐ 2Bo| }(oIeW]7vd2fjtfE|a4/Nѫ3/VstJ;dL77"(HUbxI3,l&ti &"䄁L5PʺF AǂMO$7<=CA)H݈c4.XQfnWзB߀b;ĆE 1a2O/Ɯ֥C_xCҰِ;X>զ@b_/"734TO?$ DVEdx+-uQz^zVʉh^t]wғІa}+J}56ЌX+(Ve >o*%`fMo#F6: ^ Mm8Zgg`6_'g}eʔذuUsʮ)rZ gpf*?D 'M-M:% 8z*:r@RPfm1x]/\v}믔dn6f:"Z 7OTKX`qr0MdjcZ+!(!0NQ6YYʼny e4O14 i+ANwDk"M"se:zqa6{e˶3puS|wt:CN 3 uUm-6|֊Xg=/NU޳Wym6)"#=n 9wpUf_c9ů'mz/vNHVq(p<Ԛ0.#yA˚P7{,C7g.|k\_2RR?qcO\]A~y2 ē;R?jjiP=5yAc AYd[<)ۊM ڇ,ymYʤ(?)k'~`bvoQ~en[d̾yE%c7T0Ea8hX}g {a""C6|IB% B0 fXÞP")g-kqzaB{LJͿO Idt<4!h+BMþ;nAPs@eSohxh{}wUH [Xąb$ڍ>eB] .mMb.|闯5Tul"5kA#(]}(|ՄF`'="Pfd5Lhw8AYC&<+Ϊv/YiۭgcIG)u굌u!*/"c#0֮H^|UX []\jpzPV/BU/EvL8` k mE;'k : 6= Ttk{3z%0E r1zOg}$"zp>f^|]:{",: _&Ц5 w]ѐwKr$Kr9X]SɿwOLlu{|:[\\vop(;ln'g<sJGHn9e}^]Ӆw0hNƽ  endstream endobj 457 0 obj << /Type /ObjStm /N 100 /First 877 /Length 1623 /Filter /FlateDecode >> stream xZQo7~_G3x*B HF<\w49~^$o$.8eKV 7""o Pz7OƘ,⮣g9zw!verYva5CƱhur8~j@n4*$t8ZhՑYKˤ ZNՇܖ`ޜ%7D <ѻygGJϑ%}Xʔn)+FVbĵL}u,e cs_h7@r\M*OTdb.G$EDqn}Fg ƶӣ62˱GL=hG0eF^7+v;1_9~ˋ[G2=nwx<c'>pw*H\\ TU{;88coCg\aRSM\((M̫U܉WyJvƾ}98ǜ|(kVBA埚z.A;BsخqۋgzǦჿ|~~f'xs3a5v=l^V؏86BD͈>(}/F"hmwN; Iu3kIw;Pi}{5ͣ>Lo=Z.'ݍ^!|)\u;SJC+W*ǹv;={uo'U%L?LpvTnW8R!zQF}ʔ.Bvճ+?Tgw}w8Q endstream endobj 567 0 obj << /Length 2150 /Filter /FlateDecode >> stream xZo8_!,]X~KZthݏ>pmNu%$7`rHY;! `8_$g~3n#tvyuGF)I5"JAU ٻ_/ߞ_8P륩+o8ޔ&k v8Q~ٿb%Qi˳h"JDDe$#<.g8C1i)%e=klܟ3:˪{]׵^~~hnV0euityVacY35l< 3kW?CxIB9hTx(J=>7UJ(3)3[gvlFxl-N epQLRń+h2Dۤ¢m]Օ0ЄH$ 'QkZ-IRVeCkl͕%"G =°aV|w0Iu*`<L.`4AIұ5Ldiy3m-آZ k*T 7.c4wlz6oz^;|pc[~CWzXJ9A˗cr1|9&cL.&L{6cFBƇ@c2v 0ܐP\ F<aj(5&1GJ3yG!|>-T܏!LTqo U^7;6]N%Rٛ]PéP]4;BjmSQ6rVX hevp4hͳ͌DwdSM0؟ nC^|aӧߦM;Zγr;qyn7vX*>2&Qa 2 ͝B3To:29޽/Zp <6-견:;{a{9pm lY1|ꏮ"E;u#9`N`&ƙ=8=<д߃^\sS 㿿>M"NEpݍD)⟌Ǧn5V &Lϙ${>~, V TQ,9! Zln4zw$n m]*Xx"YMQ9"ACqDwi]+Kz`#0KbsQh,3]-nVY~ݚv!ɱtem%a^yCBњ-nB)ɸRanQWanU1I?e8!lX,jéH8r3NU:ʗ*M%}I\&j"E W$_2{ 14>lƏV &OVs"5cZk_m s8լK +Ou_@쥐;49`9@ e3{+ZBca' h<%?'I;ٞINmd`f7_0 !xɱQKr$`x0ㅂ}Adu+/ەɋEwc 9e؍B&UZr܆Tm%n&[bj=cșۮYݺ1Ux Xg='ԓBtէ2KLj4ͷλ0 H_9 8" ]LLУ\d9p8?Ga,>J!H_w @ʐٍ{@=xUMĉĮul3=hM]48&~vE|asVMMa4A.L[ޛ1^0X_ >Wx9z_;> VhǶ\;{9`ucA7n{i˰M4_']pt׿a endstream endobj 572 0 obj << /Length 1992 /Filter /FlateDecode >> stream xڽYݏ۸߿B8A[Č-6)zy%ۧa!K[[#!-YNvpX`E p73t,(˷" (EYyko~1nc6m|fm #x  Q@ 2A(ȁ.O`Vm!)aZx|&:Pɑ^F S1۬XdEu=z9-:Zۙ:_ ۪i[ ̌ ]SKїofL2v}\4l*H~ծhWEەWp=uQ80i̋[&OEޥk- @h0cJD'RHUmY (z/p\nĽןh7B^z=aN&UZyO\^8x &)S u=J,]HMp&X}I"nKAk"pHy| *+$_Ž<SY(WD\,w|>SQ2mݙ/[ydb(mzod6 /t{EpԌp_Q`͍5CQ(#Fս>w/Q w0LbʎF}0 XH%j( GHh!>ň!v/$Q'# u ~'"IiG) y;Dɤ/D PYhHIG|){q(Byr v5xW-PX)ۣ@ҭ%|"=blւ7ۧ۽_vթc%ylI:L<'Jpi8\渆Gz]P1 2d硔$*x|2U~#vZ4ձڟGgE]mZ !1,cjlIb t"ϫC6b:aioB{t;tn$ '/@,}QhPF^H@LG8ihQO`c7Ds/#MPnV|ڭ[$q1K[YO6iL&8NH,?g ݡ݌e~rp("ȘY$P2M&`n p%-PVʾ7MR 2r>n̎Skfh7 /wYg{Q'Nw(nqm]AkAZe=,;QumSO,cKXiƃWC@C2_ Nf"UaV aju?GlM2{~/"HHԃ N8˕&ĒJI'$`rN\5Y1X1]2a=!3re뱄NiAR󧞸 >~oҶȼq +ĐlmהmkI8dxͶuHVϝF\렬l`.[ߣHʉĸ}~F 9}V~|ȋdmenY_9#ʃ]h9v6)蚲K&ose5<|$%#J+.ߨmR4.è6uW1Ȯcnf mW@ \J[v"xv%]=+ß[r5hwloV'%;8PGλ-(RR~Akx`ִf%g d]2niKTP=Ai4!F ߻j#ղAy J9v> stream xY[o8~ϯУ $%RҼ%ۦhَ ڒWw97j"ys`g`6=# 8F>ej;k`EVDٿ.gzGN"Nu!'wk_ӛ;9 GK=\z6[q=`K" aùhd]p>4'3=~lFxr.[\]ϵTv)D(oE.'wYߓNbLӵge]O)Md/w@[  V}1BU.I P&YZ<}J*N-"ӣ?+ga|79f`tb! X*v̓yɅs {mU ܗvPb!C~h?Oܭev]! m#oubsx8D;9u#Gt ;in(-a`r4s@ǣaVHwIv !:e.}"۱A=Vܑ!yԮ:y}^q{#%6Q)dMnP$ 0'Ys208p"- 3u[\&!JM = ՘1|)n3<_YX#cQS mNb7W *E9jM^͢qݑc%2$KʞV"]+ɱ Yjm}b1.k]Bڕl*b'mA01Anv* 2{)Qk;Ln͇߶"5[e Bo ~vG$B J1"1ĈDvI)) qB\\&D F{6,Ih}fR(AA+С|qXXU*x9g*p;hc-/ax/Eյ c{gidȭF%>?&> HዻXܵXV*J4E'(v/Ϣ-<~< |(yCD?N 4 C, \t%7u'S( IӞ+NejɪVjLvAc;@}yK(um;zp\㔰9Q 1 Zfn6n]<&QF{aeh^ C SѲR>οē>GnT}S |8""T. >^vݝb6,OiӤpgl>t|Ҕa Ҭl#_/:zZ_G4;0&k%PƂIUF.grJ 8B (Glhfmqip9p3Ϻr;%9\*H$K$ _^BE{I KVNi@ᰶ<(*z\~De_ͦϛf~-;<~e^|h`T#SH_?_ endstream endobj 583 0 obj << /Length 1988 /Filter /FlateDecode >> stream xZo۶Beb&u htkn ! Ֆcm鱬EJd!yxy(ag/g_ Q( OQO1ͼܻ||y9 E6qZFeU""雳ogv9fa w_Z{\Dϰw9Uz.U H2$?L?|^f篤l@E<61 UjGx15 (o "8uSGTÇP Av4Z6{z(G!VHc J+Ne6@Z! $9i}fg6m4m&Acj HXUj}r43c4IopLj#+OV+ܪYRE^dQ*^l ^ղuD8"pN2y ]:P"L@e2(Mֵ.HrB̯\]qatD9z ,lKVF(-6Օbn%vV$"EndYܕSnı{gH'!j8FhV(.*]xNjO?6Qc{qSֶ4hewۇŽnk&.HK܄:*pmu%j:%Gh$>89>>D "ާ)Raf,8ȓ&ځl^I@ UWKJ6'UriLu .&sPZme=t"@K)Hs\ɠ-CE#[M0!PHi6vmûw#Cա|ܢ;KόضztnJ{,lOzbbeiN ~Vۡ[3OxVfۜv"E/I!"|2?Yjtú*f[CDþ^~{4EbrT(4Ewo n  EdHlfZq!`8ivhv6u qƴLRS8U{!| CIZ^/ATa(Zr͟a6g`6FGzM (H(f9K*G$ñMjK_= '<Q-B"MlUVuiZ4mm+nZ.ϧ~<&P}iv FnH(914 K:5¢$ H9kl,S0Gڼ>ïyrA~f)")2D y$apy!5 endstream endobj 588 0 obj << /Length 1905 /Filter /FlateDecode >> stream xZo6~_9@7)[ҭEmlȊBX-{,($jeC` hx;~%8 pӓ/y(Tۀ #"LfӐhrm:*[gvJ/tRj@@DD=R`(W-.Jxt vjⶺ*jk xjVx2=nBQK[b$7j=8#a}vFcLD!!IֳW< Qo:s&ϳNW-0o-4: 9i3}-vQaiVY*)ln\,/Gˤ<5&K6ˁcH!uˆ*pdUX'k1.i}l%b!.Y nB%gv4!$띯6\ہa-X}hP`WLd0_" Of `QDXPiWҎu:P%8P%9;mcnbI8Gp~`PY4m凗\6_vm Tw P[nҮk]z8ΒEvFR&:Nw߼;F-$ށ/^5MlJ$:&.n_ Fn!.l Ƣ 1U7^ovfگ5bSPc(m}4캕 8OJHr4G&%:@£X8Q|d"@9g1TEk|uJާ2SP GYixYhpR@lrx= prJ Ed1ZO8z{/r8Vjq[.<z ֋ێ&dw;n7*hخ#nģxKPeY@YW ދHcRz*M<삄q[Ԡo Eض2uZ -R~+]䥮ʁ]$h"X`! |uWw݋iLnx Rm/YQ!&wE& WdPR>xP6FY=,{MQB㣕 0CEViU۬}XP`qjcPpXAfXI~iT(< iqJ&>=aκZ]lq5J1}9T=>{Nxn=7׎\;h¾gc-8pӽb\7Oڐ`|UV)óaq[NK)pCz0tո|Vg[_:ʪW8qT4DB1خNKPȃbA@k%5 Ii7p?_5o endstream endobj 592 0 obj << /Length 1613 /Filter /FlateDecode >> stream xY[o6~ fy'>iZE0 YTYڒ'9ɶ_#uc.M9߹}wax Jo<TOk/ΆTiu-dirnF"^P$9\=b%r$E &0È{0Q%a ~`a(S/?:M#%- 1Q)7c?}ܐ5L7}TD_| 쇉zڙq͖aP| 4!^;iCp K^#LzYM ]QꑤvJ@IZWU:z8)Gs;xMMhZ^]]?~1iچ F: B܈T"#Ԑyң+!AݬR3Y57-"j 1 YX\ `!J0 #Ȍ4͖0$7B+(lCQ®^Djy#l`$ePk5ZQK'PV~bƘU;JRP}{8#Pi. YldslOG0_[LaI9+v<:_<\8O}Ĺ.Q  -CV>J9#+==b}QYĿ:cx3 H+ lxi凋W.p:3[B Vb!; ͷ/E+ƃd"ufh`V-\^Ph͞Ki%qHR+ȮzJlEvm ]U_z6feʸ\=- hn$R{6EI AKOӠA4-݇x[L1S14)@fhT!14%0-/nj3l,\U\xbtQ~,,29TC湥âfPaPYDq(U3#BbKCBBbV"1@@l0u`\X~ͨVdH($U;ծv{>Fcj>f]h6k78#B4 "=Z.Cn`j} >ʡI nS8EDpf2^gn}l£>tC"%daF`ڃYb~h)WiPSF3 "5Tqmr#$89DVftv|2]&ݩ@ăvuX䵱ߙ .8Әai{ۙm:sԱ-o.Aح2C:/T4{!g w=CJ X=zNjʕqRO>f=F#nT {~8A=>~VڤT¾T mYӞn KZIJ#%u=*F`L7N=4Y> stream xXKoHWF{R7i&DGaVڱ76dht;O;qiꪯ mbH(ġb ]s"Qj5G N.}h s.4Kܙ0adpx1w@a( XX烫k?APj I1 g|ǀ873%LIEQ7&VQPtA~ѷ|IWˢc㑒-T !Pm}>ɆLz$ލI<쥳Vφx蛝r{>:~xݮQzYE3vq bB!OaDr^`LrDBAcH˺5H+@E ZXu=񁛨z?+86ƚvU(2!(G 1{M4% k؜7ݪ`țQ?)2EPV%{vEnjߟS}#"rJTỴW%f\X΢baS37Mib&sxh $O}J,Qf3-DŽ59]C zE3}SqA0ɓ݂lڀVi<dbU o9kF{gfa^LjhSuF)ms`eݚO;bXGYe/"Jj`~رTKA1HŻJPkuZ4OO}9=N[YUB=k)Q v-Вֿ@?S_B^+b7} Q1ob"=wPXdv47Fڳ|gU[mͭeq-Zy|$w բj*F?+vnI!|~JH,\nۭp+|.D׉@Ľ)Hn͢Tt-BinfyD/{Ok&&U7N6)튯X]%}rpkZf„#%چJ'}O#Z W^M^(KfT~#Aа Bʾ[.T~ endstream endobj 600 0 obj << /Length 212 /Filter /FlateDecode >> stream xڕn0 Ew}GHzyd0bXv۸Wt"yA\CGȖ-ސpK`C$UsZ0+sc{e (]h&w&8*>C8J뺕@idM{xaO/?̿UBP&C()]:oS(L!VcJ?7OߓR endstream endobj 604 0 obj << /Length 1920 /Filter /FlateDecode >> stream xYo6=Bؓ o]6+ڮŀmi6Ūز-ԖRQg2_%2YvAo #88pSŰw[?(9,AZ2vZs-@g $;;FJl ){!:ABqq"1鬙>޻`G17)`@R GȇJkQ8>$Wau Q {HDXxyI,V@>B GHLj@qO]?F5dAy:Mj l?wx 7#z"_!)C~\䜼-[6sk5A NgnbV_"B 6U%>QyJ4%'&;o^aƻe@0`8(%*d&= Sa3'ԍɪWMۮkTݤl~/F6dаhq=;AѺyI]]Rm@]T̀}q?u%|o)> t֬wj́1e($/"Wv*Iˤ?EvyRKnr.$-u> {ވ["~[lT-ʎ1rvy`rpbS1XN{ ,(|7So7q#%Io8OC됇M; ԇ^Ѫw Qˮ)T'MF*s[Ui΂02QUk:|BlqN\2+4=!hϟZ:)%"Ԍ32~܎ff? o*a\L,wo(c4=noms/ŐŐYnǹXY:7>/iV!'$ "T7QDb'DܫMj>6H|hPgcؒ}LmybՍ ;yIv]d5V& ΋ !|_wVӜ]'YȥB/iϳ-IJ#ˈ{+A̅ NmꉛESCh^>eT! cD5Cwd8-F &"1?@0@0FJé+6D;&㙲wCSnr_I4ul~ݙ{"a qɈPg'>` endstream endobj 613 0 obj << /Length 2704 /Filter /FlateDecode >> stream xZIsFW`|6zoLyRx}8)N I  Z;HJl0^~aŏ/fQH\I)D H5OmtVUgWNu\j /~a݊ H! M8FD h\°KJ1$>" ᖄz˟ewBL(w6nڌ!JY=nIVg"NkؓYdڵ{(ͻw%8kWM圆|v}맿rD07T?\OȜA+_vVmlM34'RJzVNqjϐ5>l>=Qa *P̄o(A P  `?`{>p/HYӄBD`'-p -{=|RhfT>/B/0q.B6q*϶iBƹQ$ }–a zݹ1i)G]O9toiD8agdpC$F-J6<P!lGHpfsхT|kL "8}r>e1D4BY'u&^5 uc!RLésf<\6`4% CËUdkϒFk4e0H"GaL J1Z8\z{0dK&bzc%d0_#! !SNgY;ȕruރ,K<&S.2) 5?촞/yp*Iïml`vE3!:+֦)zukQOEbp{|k5179 ;DQT>-P%nJoKvk&>b+SQxrR_pSAg[-6RiAy4ݟ1p<-z L4h3 +,5ǔBbUELݕ\Ƶ dJNnr_Mvxg˛3TL5ʞ{Zh 9 !zD*8%Qc+c П@Xk]}MZ)T~*vo~<'ȅ݌R2eS&"b & +% *9$Hx*֘kIQ6G@Tr'>L:0 EsǤm6PZE|%4nV߷?4naT!d|N!? *_~F <椉BTWǐKBtr?l/-1!:A \ <1X̀G_}4VcI{K5K yjtHtxơAOOvJ$@mp{ܻzwN֙/HȂNXߒ9zͅ6jS`ӯ˜<i}8!?]/]2 (̞x3#ch)ph{so+?P$!i^V:ѿ endstream endobj 623 0 obj << /Length 2257 /Filter /FlateDecode >> stream xnF_B m$b}J F%=g.IQ[`@Μۜv۫IIysr'> 8M~퇟fs/Nemyf+ 5\?x$$lǫnb@:̉=0|{w,ѡOgvm dċ#XίW\Q&=v a!XBł=|C|B?xޡDܴ\_V ^ k5[Y b'BU.+@'OPs7wljkp_#%IjWY5? r0jTW >IYDG%#<=;+cTꁍBJPq8|2F'N*5 ҈4c$5&f$E'O8װ--(5!#[~=(B_Fi:;ۋK3P\4LQ^jLd^D4zBys;|0JHc7֏LEqHpu*'QtJ:AFl&/=}#ا#FKpeT,dAB<ƛ3-nIjK7y_kS;Da~`fnEiu?h6(uE߉++I#~,Q ,Tᭆ]zų|(!$%cZz.o5(g R/JR=[L/s /hhP=۬7J*<+i e4?sK s1-M\1cn6v{B+Ǎ "$+:#v슥~ze< AN 捆@W8#4Vye^tKT8Ӆa}C>a^dcz%L],:;eR/j^txW^~6iLئpl>&9#E_1U͛f(7xS^\+ _bK.lE=cǻ4hXO7O$^~Ѷ!5Ӝ52@7bvh^c-miQUۊ C,of8KoL16a! @Ql-DyߘGe8h:p lk {d/4J6Ly.J˄AnV`i87!H q!׼uV솱$~20?ħ$=7%v~?ZZ|E#V}-c~ödBazmDT;]b%wk7Èu.{jS>C1xm%Ѓ-e;֏NW?d۪Sn s>Uod,;2ZqQ~,q|JR=v}3R4dQd54=E s0ZMa+kng"J7БNiTSZ8)/P`gXmfxoYJ7 UZs) %lrfBhxQL(h#q:13(9> xbz zrNŨcUJ8Q | !r3}3t¼>B F$ޏWҸؒWTuzĠ7Qg=UݡcT\XLQ5y~0^97pli?c';PĺT:h SOW͐Q\Yy #t*8DDHYRjc;gXvEòa/B~GX!CyDhH?^dzoca㹩A}đo;;8 Vs;;uáO8V%%+nπa@ ]!ě$f8Ld bu endstream endobj 632 0 obj << /Length 2534 /Filter /FlateDecode >> stream xZo6BX\C*zPSvt%$w7 I=-;vdS93CVス?yE$LzWKO1OE! ZxÛ9S"ٮu^uZ_۾:q#˿]G-ENDd}_wO(>Qk Jμ/~𝄾y_T*yĞ;nJ}fJ}y,"gPuz8Vm ؤCQهmHƚ'|&|,ʻEZ~Qus7bԎ`p VIA/b +1Dp15U^e@!= <5y7)c :({MM%QmX,K&fg6 M8έA8lE:o')U:I_Vu.tʶΕV#_eޛ9(F( %*lD 2Ibm;bDž ~N)ȠmB+ʩXF2ΪI#MwȧRJ?WM"zӦUfhUb~!3X \J1#Gm6SXtd4W~(ËMY,۷I%>u ,fwNn Ҏh_t&=l-D=@;DXi`P:i}o#CaiPXU!L;ץs a3B9;ZE~L} 0 ? BuɀRFOO6i?V* *"H˲< ~ JjM-3^BLy ]tbܝBatDh_Q'[۟z9;2KNK=CXu]Z{[^r[shYx( r!)`qҢOh4h &V(,s`,F@֜) 9S`Q]hOBa@X) _meZY4 c0E-=C-4W6W3^E@&c9m)չ;qk2 rɜ-GlLJºcen+Lh(1quξNkV;qQwuxQ(!m /= +޽򏓶[Rj15.P&`F~ǟ5 (I_5O~od|"o-U8TfEUAm>7 r* endstream endobj 638 0 obj << /Length 1423 /Filter /FlateDecode >> stream xZ[o6~༗^DQ2+ VloYR-ɖRAXsΕ!hz7 .޺ )<=cNh4An_9wH" '+vFa 󏣛6Q$PtP "+})Z WP̤kX1Ij;`\]rW+#z sW"(& =ܥ (/īZD>`aE$>p-|u;!gU ^ AY q2ӫ[w+Oh3bTV@ ѡ+,_\f\DЊc,l|S0 HH%$2߅GӄfB %K$Lx 2fiV"\ id0ݰ]EIWE&RzyBZڃ.$'lVټ=¾|fm~]K?6mv-~^[ pyUϹjpv{{ `_HR/͑v5ʰ4e[o >^=yi lBbgB]T%7S(}J6M>d3nc&|$Y$ 1'$ͤ -14Ah 1P24U}Z֜S,,j4HO@IP2m&ORPx fMqf_ٱfQrWppbeW͘6t{ƷalmsP~gf>A!jN9ߚ_ !5*ovdOtgm 67uvo*XK*1 bb4[SgMZ$(޾.w^Dqg8L@fgSq53ߖ45>,tpԔUAmh=7Y endstream endobj 643 0 obj << /Length 1937 /Filter /FlateDecode >> stream xZ[6~_!/61S*f ݠA(Fjl+MP$eI#۵YF˹;onyo1%2P4PqXĂE~oTEn:"eN*m_({{obWH*H778 >56Q%~; qC`޴.EP01sJ2=#aiY Tٲ,6-Y\Oa$06F{z $ 8!H3W)̃RKPZ`1B\4"{9(XKG/Vvz]`L05l'~e"łF^OG X, BQ,bBȭtZ>f[fk}BEDQ?"ۤuy)!8St aPz:OWxL!N&9援c{=VX*_C,Mr۸Y}̶[%[g?VFeZ^];U/ְP(8Wld@Gx|RB'nϨŮӸ:)/q:q{Z/&$PNI})${̨uzG@^ۤs D>@V䣧=z[??AR/{tK=v]%GtHSz|bm?suM@\(7$}pŤ*QCnȢFΐ(<\m%{3LqQg0_ӳ4cER'1lo&絡*ȳ Nx3C1U E*CnIa݉1?^&~p+z>Z9 k.A5(R; c#{0lp`OXc?_ Fm*O=jxU3=A(<4´.wi+[6h?ݕNv@.BJ)5=V^M.UgyyYM{! hKoݏA.i !t*bdC*3y -amtVx>d{c9'u 1naG o*JNKG>cׅ^ _CYYu ƅ{w,BSIЏ5L\a:_cn V6b6kEV. rW'܄jX1u6Y?XpUܫ8񘉙PBGBy^)l؞)0^,]u߻k-B@*[A'”'-[L>?*vlI^D!I^'\/&ySppx&<#iH8|c-26BFd#PIz b)z=L9<4:C$&q}o=ċi3F2؝ay1rYn"}/%xoEowLȏA?FJ#e~b5W!Q4]RKM#2slJ_9J4%7뢪<& endstream endobj 651 0 obj << /Length 1849 /Filter /FlateDecode >> stream xڽZ]8}ϯ@>dƵ vVjJվt:}hOY 6! H asν6`g`{O:IՍ29\ ׹/}w\ZePYwACٻٿ3`8D1ɝp= ;`JܛQkcQu|}a |3Gy+@xۓ|0my"'3Mh=0.@CF$ϫaVK+Zt2@E.)_1Ñ xl@G#@1$y["ZDp9 6N9A_ 3C8 j<1MT8IU l] tp/<δG Ɯ\}QNy4,~b@ /e26IYH a۴?L n^62ӥQQy9:Mq&ȧb ~.W_>!?zHғ N7ܤA1Y}ꔴ.w6F93P}Chk4 uUPȰBh&.X_3赭b vT LPjG~jt>o%ڒHә9Qb^ߚ$ATʰU @==*zB\%͹PM2N mzPk`͍b2-кYڵFvt#A0Ku.4t -D3VX'2v hjŖElpcCв ؛BE~/|lWԱ7|>,l~Qb4neuYܪzN~H+0#;zzP(kk#Յ|R:#!+fi@V9CyV1m9D,ڀ6rQbcgakBq CVӘv5>TWUv2jޓdh Jj !!g~Bjn 󌤧PaFDf3:et+~5vs04M02vˆFx;ͥ V\h~H2a*i+h uÛvm@jN>MT?(/h*քRFRLO }0>gbBГc`J{UU aH;veZ8Aɝ;t 'd㊛,_볅B=b2.5ٲ۳>~"VxO X%}&O^ԉҬzvPޅBeP{\ :+P`ҝ!w! "ғt N@UA]QD_ca""[g/?Ûa endstream endobj 660 0 obj << /Length 3128 /Filter /FlateDecode >> stream x[o8BO6")R9v[l?OE ۲-T\Inf˲^zAaߣb@D?|o =(Jμ.{[.Rt8()}Lv?3&4 7G! d/UQnHsPn"aŸ, X}jyd)'e vUi~;q|O IO|]_,Ff,D̛QE"nw,K/u~[`DB'ƙδ9yb:U40oemlv^dOOVe5}žc|9/͏;\Q˴LuQ>t'ggS:ڏ` DÓSUy.F3FYR=Tu%Vz\{(5iIDCOF(`"a$2V󇤆V<(! Z A# jIS`8> XꪊO@"|V0Xէ HnaOM>$\>ÙI~xY$LV/5}RԩO}Cz h; '*ַwIDT@mOD sq̶W-)kC 2~8J|x &Aꍭ%xŋuCx3*pao 4n m>aJI(oܻ7;a En|8|- T<! |$PPc4 >c=mяzmP{l^Z]h7<'(Є^DDf 3Y2í67Q5*ŝ+f3k2$?c='YKp֙ulwc _'b}y>t5d7b`0ܧpg 2EFgMQ*S}`T[ݡ Dr? S"ZljT0\X!2;17 Po@FRؖfxk(Ȍ C*mHV& °%A;d0BH x!-]YVLaw΀zt;2O :%T3vY(T p]sRĒe&c ']ÙIཛ}Ҧ. 4k.j#`[~'c+-N\ϹqfsJ[KS ض%HFME\;$l3^#@ ƉgdL23uP`lur?v!H?% qό\:bH*L@mOXt*aRx)\ }Ħ?\#vpGL&/|Ms;J`:7> stream xYK6 W)En`K =;Wˏ7nSö+6^h@=wq@[^mt&@:XȓjRslnSz0ao7Pk3q(5tJ1!҈tH XZ~2 -9@IKhJ4p $y/x3nG2^o-!E }?!Ȩ>""*}2 Nı?<.xp֭hm ?7#h&T " fF`4˾,+zDϬ= ]7KKMS2ĝMɆ+uvde&zpyifoGp(d2rl|uS#kCChB醙O+֢{F,uL,XTfQ<7VT ~4ȸ2"S=mem3Otr ]enE6F%޷Ը KS3(jlI?Q endstream endobj 668 0 obj << /Length 2542 /Filter /FlateDecode >> stream xڽZmsܶ_jHd&iL'ۣx8 E {ێftX.p{uv I(pn֎=ܑa@snV_wd2XFeUT%yvmhURF8W_o_0CɀOx(xs묀qΓnq|N S+xO\J@c҈1Eh9udzPA]Q2oqz%Y9jt\1xĖ*3/.wb.l$N*H Tۨ*h黋kCuTٮOI;K _WC-\gI% =ﰚuiV(AJ+/Q* ]]%v<1ӟ |n vLVjdvlR_\eIKyF (PJkGyN BՒ2ʟ3{P3,1{<=9wA%bt*٦ʴ*t*7QJm8`st˪.T r~s7i$ Cjg>d \(뻲JPζ)![Vw\qMYmU0F(  sN҇~ t!~I9ߨsRlaGH"Bz%byW>K٬(~&4eBLlcS]uaԁO0<-8}d[%ٽaWʊi.B~{]r\=٥ !KI$γiWOO:a0^;a4 LR4+dUUeD49o(5mY};1B)[q`&]XԠH) r LpAQncХ2"8yJ-8,<e$l-T:igI NelkJ=@|UUxϿH$zp Ð>C `.:N])1)l5͊D7;`UtnSVu|0bǐ4ʸ Ji>2U]@ ĝzmY>%pxѳ<\$f$1rs%݅EӠ@ ,\h`4L]-L\_."dU l y}I'πip%E bu}HLQFY|HOf+^2:y/1G.VzݢcN.6"ߢDiWj `픅J.dD0 !ʓc.eNaƘ|B ΈKh{='aІr&Sh7!GBJO?0PB(J&QADS|$+g+U٪y6J0WB)%9!1 #H(Q1]g;ul=ruu jśŌmQt=StwO)|v&(Ǝ/XLu`r}~=Fkm]I^ }S/vf2({!;vR3_d.f'3 ѕ;G!aXX*/O}'%h%sR#& PiO,VsrI* B]0 pt#Rb&)AmIgHN/Q)yܷ% 7P>3b q'[2QvLg>`qް*)o]ߜw!/hw6y׆>>ˣ) Цaߞʂ%uU0v99  Aut[No_}}6چX^8VPE?:"i؏(nM|1H:!;A ,PxS}

bk%7ov-M]B f9>B){"s:u{[`ܹKo5T֎qsJ1F,7~ǵLw,/F^w'Lun> t|Vk endstream endobj 674 0 obj << /Length 2308 /Filter /FlateDecode >> stream x[_o8ϧb%nr{mX(6 %$_o3"%KeV1MÙ 3z>^|{qA^HB͵w|a@D ۹u?r5~0G6۬LZFe׶ILTE﷟.~,EIT{ߩ7O%" ꭕ'#N/CTD@\A-q+q0HaKr &irla?#91WLM&O>*Li(yj0$٬nG)Y9ZÚ8C\.\ĉA SH}*yg˗P" B4P TQ:YG9DW";c?7VVx`(z:V(w/п|yŁr~[m 5,ļC۱R0T# Zg}>2e o-==O+:3\ߨ1Q|+kt!蝢Hv]$b&5f>@[ ب1,+Zؚ ܗb ݦ{6>_6vt&H=-r@PCA4x OigI(*=/4RaO,>vA?g5I3"n;`~ =ZӏG <,qO\Ot8f~:įV=eV*MQ*Z}!_dZڗkA$e)D/(֖@LE+a|9]iBN KtlΞԳ/قÞmW;R%0m.>%H?AiKBp{&A}q ,(I(iVz!9mO"D୳VxE^{"Y98{Z᧮6=뤒WQ[=^0^M5[Z'Qi8tX, U['2MhY.X0i|xm>EŬo>$adkۯm} 훐8hVshA)U\ qu0 "dr0Q rp|:hA%õ&{i bY' }de c,aVU3flUR۽I-(k,)[cUݬX۠UVw˪pR]7E6Z+qj¡]9T1 Ɲd ?-3C& &ŵk[yQaI=TEG,cqfA}VH56+;9ŀi8RVkjʉ3QAηkW}KfE8b{JboͻJS~]n NJ"Dfg,&)Xn [#,M9ɐ`&Q[W AER E hg4)M<8^f<v[˜Z"Nܕx_<< B$^XG%MDG;##'jq/.gʬ>R|6D)}?1 \n,7Ul<*#R' ! <ߩQ  zP¤!|҂E{d ag/1B.OXE ~U6 ڬy˘HY?ti K+ ~vrSFd:}]p2PB6 Aa[W 3 B } 'c\DRyB2kWNv于*dk/hoeW2;YН}G G#jm>; S߽rnjډW;Dȵ)<'SKӉ޹~t:L@Ru %vCg_bݒұŭ;M1),JkIwͩެjcK蝯'g1| zf'~0j9|L {' endstream endobj 681 0 obj << /Length 2910 /Filter /FlateDecode >> stream xZ[o~ $f>4IPӓOMaP%HߟٝUj =\-gfg͒?^|w}iC:^B " Bz6w/"- aQLK˹bVٕǙ}TUdt-2DȖ:@S̸!Qd˹ ٻ8GƋUR#ﺘa׌lnYH`ͤ1 YKzBkdcfCi?mQ:yЂHÃ9l™erl23]TlSe+W68'\4Om\:)+kFnf2f'[I$[uDK<:\Wgc[/N)K|MU3g{A Ɋ\D˞RK#|k ox3&1^Mj[$rg9XZ`^p((%]pog&O?B,v5gco=$/5{zX?葖%!oL& rYལz:}X;@8+ׄꍗ$3h023ܛȇLy2wcmK" 1Br$$ˬai_^ViI Ց |V~L̮۠!~Gvy'#:L0O*P1Q8(=n.I4BҧR-7BjWoR[H`A6Qg (|g]@rá4UEDaG6۪o~bSeF9 §/ 0n8+D|b`k]t_ˤpcW.MVÂeg&>{H\ؕLk/c+ʺYT8%*N=Be8Q؁yV{3\zͽ\x8-Њ-UiX7:?Nċ}c':0cB9P#q~]lo1ȴvT>? ҡoM^`ˮ26hJ:{},/-h"]dI C!Acm8vFPڢMQ}< )V_`[-}UyM.ȇ;w /#98K/yQn:<5/{G,QKUI;Rd-n:w,).]09LK0l &]yp |0)w/WՀioxMGH+S0G߼uUHt{tt[TuZjlJLf,*<BnVBvzUꯏjoR_CQqJNH#QD}I蒰2A ZDaĵca:Pni_6݌Pifӿf`UbõGmOk@dYo`n[PW 0ߜͶp[Y&f{3~5ڤJ`|XKL7ik+}i'9X J:2/@ @[W̔"Ǐ̘zL gf!'qڠ]-) ]d* P*b|u7P, #Ӟ($BgK/< MP6W'HJB6|q82iݞ`uT}3jfxº= %d| u`p0JǴyk{+#"~gly1I:] nBw݄Mөw$9:T)k,Ştea{Bi6ŀlOJXAX;ם"+4 \8$YP-'^n6ɑN_,j,yfmsGF-%˒/LnnQhJ:C6{ȌRtsK'}~ HFfo\|pogiSQC [f1"KAKGmxL5 4||GT}G}0d f3\Oo *_LjgPu zTONŮ=U\EaQNWn1dUUyӭvyՓ:0|ѷ 9Ѱ?ZY o@ܶ}BGrP%si}Woϱ%HhF\U*+6fGy񎥏}Ǻ/  `EMOkg$R(UdWb3wmvj>o8A?VP~'}}b0.˯Á$} endstream endobj 689 0 obj << /Length 2396 /Filter /FlateDecode >> stream xZ[o~ϯP`1CR") t/'@ws"юPYruI6(;jq'L3\ ;k;~>NNsruD#w>o *<72*ɳs3U2,ACdg9#;P mξ?;6}|;bX;3G@`d(EBpR f8ʳ*j VE1wB)%y]ڑp-aY.s?17#. J35p@~B:+X.Zs|',Gڵ"S/u" G4{#} ]^ՆfJ̱:$1+Ft$I_B`J &ڢ"y-)ϟCvG@.%Y륔 ͠V|x_6BMK4&,*oS4_ĝ. i<䵹ܔ[%r*Ku<.dTÜqIl+snga^NƌԲYi, 3 7ʡU1䍓*dɤs= kDOG[aU@0?i2yb#᪀4A8/G,|A%}K-˫/\^a ^ B;bOZ|Y7} O49rKB%*@N<؟^d-$h8Q(ìW6Vf nd] @ LMmnHɴMQلY+E]Vl)<9iFBH4A wٸ.ik!:KBhkU-_8`UU1Vl4NTwiRZˎ Fj(y;.A'lzTO(Lw䧵,w[uU+2oq6lnZ#ӿaU"*q@ d}bD sgYneTLհnu\`餴3)!k?@muV@:&> stream xڵZ[o8~ϯ5+RAgvf}Xt2Oe:F<4ͿsxHbI.D4)Cޭ{ͯae,E XG^,Hf}\_"IWNvE]TUC?o_rsm|{Ce>{[,R^uˆ3.߯~ 89^AHOBD;|uW|_ ;跬hM] 7}+7{k,ΓѷEuk67&SL[_? ISV* P!Eɞ9ݝjFZEIjx!),56ȣb3>K8aO@^u'o>(jrgFG;Gs$H4gZmy=`qa4з݌֪W&2̪6/˲J3x&FpXEOy&ӫ:*)AƂ [i#aF"n-bݨ@;Aj;uN~_{jDp%=O堂Ս7{ks5,.}@`0ݧ,%l|XR'/ę1xS=L?l3'?&a`S)6"Z+lC ?藀ea{ vX}DȒqnF()O?J(st)w-R,N 2@,j웺|7w[ e_v?|ӄUKU5uš PezkQ0nV '8f|V Y;zKh Rsi@d2Jَ9]aJ}Z ?`pymKޘ%a6uO\/4&w)T|6oiAWEciC^Hx N;Fkкul,d^vvSE/s6e&e&5\KQҠv9#Ŕ1zIJ3-Jî⠐NPwμlf$W\a7ߑ8 ( ",nZ34{y*ڽھgWY rzuu,0ppHi~Tb)DpIGgy[4K'`w_t:,Yjlh{tǵV9Skr/>' ݲgu fGwl}Y[0#(eΦ|\'".U7Bvu# S\ ).@EuEe7J9TM=mڶ튯KH5eiN*R_TAH0=Z[;dW T(tH@)ᒩ]X31`lC.8} e_2㱦8$94EL!vtR tKۡn@ N[i`(0tOp}?ݞg,*wPN$F:6~45՜'< ğ!oOϜUH.X:t=j,L곀eoKk\ko<|CǰB!h` "B"a|K,%5oY_(<{I..q[q[:2eaKLfdn%TwN@}?>\FE~XYĀ@ 1wև<7}F4,xe'N+L񮜜צծbd]9+p"C]xsVN7,t Xj/7b+ endstream endobj 710 0 obj << /Length 2646 /Filter /FlateDecode >> stream x[og Xkw@. 6 ̆"U>EI8 \-3{~9 f B{?z//$_j2*,soUBq/>޼9{ys3 |[ 0&}o o<Pzfc#'޻NB}t?{HQ'\K% 0 (<ܤHѻ"0ȋ7ug:ғ[nCBr$V(rUBF{ 55 jtf p-AϾF $s4.?ݣޯ랢1 8=t0=oL| 7> y+fq'~twn~>|=Jڼdq5 }q(CN=-U>^ Qҳ\O0p )و3mVπN/Tj}}edߓݸu̲U:SqZSeִAc-eëN^yonjgz$:Fq $Wdo5MF!F¸ 7X+m8ۡ ).0Jw՘6R@3n$[-+$Dn}?alxk6ϊ$ T6S-HTf&diYy\*=MGb޷YŽn%Y:d9 +>٪r} Yg8{ T&9kr ;;6^?ckZlON;b(ƾH{[l*12Σ&B\\frp#0H۵Z8-!xtm>ǦD:v%FNnaG_3  UNuGkeWQױEIX踱댩4 Fi$*=1 I_X%.eekA,!~^@{'uygupb(o>mC4!O.+ʧCs- Y/G[ι`!9$Jz`]CͪH0Hښ%x e0Psb[ܴ PU% :"96&%aZ/p? H"PStAjPQ6zgC ~ h*:W NcGC&K=NJ{pp=8gp'Z [x&08]J*Zd +aҔ0#FCu_SPm 7^BPƩ|OCM.R Eׂ՚gP|rF3RdϚ<l1݊CB0(=\9z6oj\2GzSZQk #8 Tv4+k`!sF=c$A06T B咺'wfA`[pًq=pL`FM.[{`w]eioQDpVB0>.|}m`.1CYF9&PS\4vsfAֽS(l"pRkY^"[C#JsB7X{-<fM=ӪsxAHv?*.Y:.VK77D*8pC:2[Gɂ=(N7F,6͸m2wJZfF ,l,ЮdjktAcUC9*n aċeu`߽xz3VU#!"C=nDH :Q/ Y,Td5my[=5l^ot9M}чI l ¾h4 ҴwYLeC?̀rQޟ&133RX9xǷzRm |σnlR/=-1e0V؊?G~Q l!a&pIj(s|Ga>W9-_:['8Qa۾*TMwG*N/98s:9Ąs7܋he d*Ɋ"I¥/p, endstream endobj 720 0 obj << /Length 1737 /Filter /FlateDecode >> stream xڥXY6~`dkHAZiyJS) Zm5Jr/]uvX`Eȹ盡!ެf?$d!ZmQ $A}ŒE2=T&+;Kr%ke0,0]|Y]QCE$Ba AGIfqA1BXٟ3$}Cv@IJ'R D0h:s ^V mA}>çq^$P v1G8u޿5&$d^  ڮ!Ok{In,W<'02`gB#w~ika(RH14">6:Cn`APZ6Sfe#c2)0OY <a1ҫ^ͯ!``",C 2ߔl"-$( [lv$9sZlQIVF=1O,i49V{@ *-eTzԨu#/71IAkR@9=KpVl @5Y)K:~eȢ;=Xf^euyMA1cN#KKyb}H2YPȊΚTd0)mJh!{u؎AK!{5!r^LVOJE>[0nb y[KE51]ԬY\HIC.:]+) |l1}n"m?q?DERWGk9c{3%P(iP.\01?9y85Ltj+ Hjo N:>04Hn1Zu<ҜNdS8E:wYZL0MXy]ڕ Qi;em:(+:S$LXlY9Ylɣ;zK7WDfDNVgom^>k-7ۅǣUJUVS{4ܵQU3Lf1FD^gzn,DlW8Y5ghK.7:N4y>0T_ib'+^\=6yjZuw.AaK_kM-G~ډ9aK 9)SU8 o ^6c\.dȎui?~z--, J0*, ,6͗*(QUF$܂Yu8Vh#·}bSsc_@DahqpasL<ĢEQ!}n6L7{_Vƕݎ.3Tڔ@Gn1 'xAI8<O@ endstream endobj 727 0 obj << /Length 212 /Filter /FlateDecode >> stream xڕMo0 >j|JPA6Z"Ŀ'ka.l_ObS&K3d%@YHffdlM8Ujϣ:T]HF|}3?pQv 8^G [ (-} logy_PE(%NK?Ajdٱ%dž([mUQ<ϐdM endstream endobj 802 0 obj << /Length 1476 /Filter /FlateDecode >> stream xZIs8TMHhAgY|*WE&дp ɘ%sq)Iljew~߬/1 ȺZql1./|ǛnZcrS\s~Ɉ%RD7mk-1^^5Xy2\m(bilײð]jދ_5"ĝ]k!k'DBL*0a\n~ p"LS" Zs"H rk'"b6Ã]FnzwB*6+9pD(Ad0`t&Àc?.T*r-br?~IfY6$*#~%A%D֣dbD&Ų Y+$gN+l "Z„X*`H{E&!KoT)F |eQoZR /=JouzqRHUY%ꃱf^ml`kא>eSfa'RJr%*C_GOw"?1WBR⚄t*IVmzR:Gh qqF(ۢa` pg0GiG i ƴ&Q&d}) oAc'4d%kbFϏ;5>OJ-rϗ;dmM*M"`n)Fzalr 7MgIJu%'\~$)3As2z&a^Usx==HsWR5!H!-K }<} 2EaSGmu)cQaKA_౩̂t,S%!g5 k9t%oH1dFx0"r,0Ei uy}^NpD '6n^>#rEXe4(Ɏuۦ+1Jǽ7*4ElFTp8l?"0x눠F*F>Gd)UEUS9<p?lWeOMx'2s9.JPQg0Au呷czզhp(J;!^*xC%NSN~3D <f JfFKLrbU*jpk0B C|]ߡ"6g-7-oFHixfpԮRZ| 0\-|ZZBڇU5hLsWA%]cQ4O C_> stream xڽZKocCoW׫  H!#4Ւf)qNϲ曚z|U3f^Z1BG-Eż͊s[8Aޭh%4Bmb hJ^8!P,ðpQh8θsBC pC8BJo$voNx oTz 7@pbPbjXDy9ЛbṰL0GkXXa@&*xJUz.8u-%d-K@;ҠTr :9s('tq. XaywyQ%`W,HE^v<hXbfaǍMg8O`˘Oik'J8u,jn2[6#RƠ%wf &e.-C/8v22@\{&qM)Z"#3 AǿCF Fsyb ZZvc3Z&#).f KR pF'=yۿ߯eO|q] ZqՇ/-Ioe+%y8W; "^^^e<}͑nÿ/{s?_]^nߗfrjCj#kDܷɓ^zqUO7WX& *|j0 ]Z'c=e t_^}JĪ!|G|( gInN@Fēc? 9}x{,0z_\wƗ˽BnA|a&;#DKBqJwdGV‰- \'9wo/5G&_?EV-7F_^uJ@!T}zoMo1PhP!z럛=, 刹f s wJ2ih~U8:޵2R\iULs\nqlq/!?ixzбhF6W օ뀥р. g[Q)G'X~EǵG~vʟ}o~ZlZ I117eKCہxYg)%yh <)30(9Qm>ĸ2Ƕ̹yy8Yn>Bn^}иP~RZn}DhZB+M~nkZdE۰yC㙬("#?T B3 endstream endobj 851 0 obj << /Length 1044 /Filter /FlateDecode >> stream xՙKs0:3DHGCtqD@=ewTI\42 k{*℃w JB_`6W/ϦDɋ$,Vzyk:Ag_g˙>BWW9@W.Z0$c{]BI%4Z}F|=OJ, [O*9H;/^{jJ`nb xОGS:** )Ghz:wXiRz:.Π`Xata*y"@Pl&_&kϠf25DO9)]hĸe^5b eH*y\"D#QVe~'P&Pinkw`5C:Ԡ5Y][>*e]5$1@Q kM*\ |Z)*5 z+y\. 9VCuH#YF> 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 869 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 xmR}TLyQ{?lhjԘhj0[s3stT,-EZۗ|4VێJ"16% mB[vo=g}=>qb %Djʢ39`bd2ζrt㰔@0OJ !Vpc2YVq$JAyJ)bJL|u-.M+a D"JCa (K@)9B0A$p !4! RX$_[,|((lXNhFh Gt+ @28 A!zD Ґ@p$*R h|L0(>GHd*U!%dR Eb$AMZx#!DnIO_0Tq"<@*A Q9!hXI~Jqh* h( āp&}Do$|b-Z%1W_+8CCqA&}Br&zKmU. o_3 e (].'ڡfǁEaʪ% vGm:oy׽sg=;4愷M+m˥=h6skɵmy/e>Z|HPlQ="&c]Y25;*Qf~`WYSeܡX^C79;R(&nHSfC+#wJ荷$qGۓMbZDyYVI~Wp.G}*<Ŷs =MSfYK3OCV>v\8wy䙣F({ :ߑli(.^׺7g!*is[gR mMYRz)Gؙ캙wߣ(#+ѵfIWDvuOЃcsNyE3^eo[_S|EQ3NN {k.pX^YOMg^?#=>(7u7EaS{CP>77౲WR'_vIͧIjkr(xIR5vk&)hٶ1bWw6y*dS۔uu߭)2R]IɈo.?ħz]PƆH kWVCweB`ʎi.0 v )ŻV)Uܸ=j6|CΏ endstream endobj 873 0 obj << /Length1 1606 /Length2 16112 /Length3 0 /Length 16947 /Filter /FlateDecode >> stream xڬcM%\V:em۶m]m۶e}Y_3YɈ;bGCJ(D+`lgh"jgLH1tq65 98[ 8pL&F&&#''' )@@FIMMBw-_J&&gs @HN^CBV @!&35q4ȻZ[-LlL(vXl-)͉/dobdw?.ooF.k7`vNNF΀YE?,v#\)__^g ['? MNsw288[89OwNVǿ_,LM`4rA502\Mm?3C^oJt}"7H"oWCz+ Hc6f O  `kW :0Z8Z[8L __-m#-li:Ll+˛^HNCVVަFUY/QZ!(hea2s8>l0Zd \[#;D`/?n#Gǿz{?wez/:ܑ)a>FБF^]Jv3Iñ>tkT|\b-Nv@zR_j^K;`l uK> f:)] QI}к ɒNȇGG{opsI 0|S=\]zQgca%>Nly-Q L~HB]UěV{Hw2j6B&-EKOMnI&3K*m0]|78t)Bj~ ' 5DT^O-tf ibUq):xsз3^LӼL,9L )8d`v0'2+T:oZxacʥd;ʡټJ5_粘qll@|iRtO^x{{Ep 7v IW]{uWf~hR&+'{t)n/\^FF2qDĬ{YS#Au @p v5 ւώ._P!r/ӎߑ$j6\?wM)YH>8*1ev/qOMTYG' !\A kN6P/ED{a}G̠+ǧ13!K;~Q`Obt;<9~hY|Ӹ= D5 B CӧbF7ۄMp|<^sW+&U :љa8t£|$XWyx1\sWOه_ sTzU8~5˞|tF(t7명sf~RC[4$ 8~O{yh _ȹ?=re-~_gms) z{];.gYo1DNT!. ?FXR3M?,*.%iG݄_/)٣lbpl~[%e"tP9&ح*}q$.J ol$"S25-Į8c`٣xn 6)I,/2&ND:f*hJ(&SwqUq)cXEV ^̎t 7/Zttg6lG=o>sXZ"If?ep9*_0ʓUƘh0$ExS=Oh2e/smLY 5 v3خ$&Bၸ!RXna MyҖ'F"&~l RMYѳf")$VP紿Fbg㪞\a9W@yHxNZYP$5iJgKeL>|9 @R܈*,1ǣS umCF{|Rv #$2>Yw ~~Qp`YQlshI/4_yz|Tml`}l#sTw̑! |?Tp7߇uґqO(5 ]v{y<_KQL-w·ISr6 nCՇŵ05~r8)):M:<Ԭ6g : Ga2 u\<eW<?w?%LV Ko8N͊1RX>LLkrsc0GIS^#X?$I=ww>@-gqMKSrh3V@Uh-|Fq ؜̪ Ʈ'c{BG~S5&=ԨJlVȜˢUJN}V\8> h< 4K ygL+iAڶG]yn^9;m`U{O>Y'ט"\V[K_)%ȲjD\X/QzzKFa߿*+ß[i7 ÇU/8 8Djm{+xWF0^ӘllpLrGi5q6ba^}YTѦP2<jmcϵ<8ɧІE~J/ vGRYZetrKeHlsE8ˤBIF(y+J^._<XWF+_ 3.{ȑ |sO__S dp7/Ӑ6JD)J7RUĨ0IT= "~\~`@v;!qn &HP˅]%tMRm*k@^[$AJr6Z(k,z &hz飾JD3@bV2*j qM?]F+/>B'J?BIF"Eo35Q\j\HيDAE@f"l=7eJьk58÷-8˨W+*0|4$5)J]X~}jz@[83kG|,GYq7BnT12D;,!OF`duAX'/%Mf~3J' Gf4]/ ?4@/ioؕ7̸sةq9D0 ~PRgc}; NY#fl|gF @U8&#;Rs/kj%]M׭9@UjoMb)t獯v™RĖ{լ 9^($pۜCI`F7Qtձ+~ ta DQ Pd+ g+I4:d:/ݦ1O"3BDCZ .BS'EUZ8jSZAfώF^NXpv|=]˒U /<,gt&=TRj󡻭:1$([5Z [f"["-k(\l7y4Rar:mP!_)z2Ja"h펤6Ͼymtx.0R%Ib!zI4u @mPI; ܵ4e'gj{g7'C31s?X`"؅bU }! P7Iq?K1pbBh~Wo(C ~[~F HH7hɍN_}JD :ٲ>U1t t?5QpJI=Y+8= ]OU'^Dh!@6!BL%M0\P8C@mEvl,(YXEz|S7I΁؋]jm Ŋ5ϱiC.!XԢS·؅`e:N~s(R42a q3|cDfr\9F `"Dҍ,1bY"[W *> -\˞ϧr,uk,aWMZ@3t1A2pաc3-m^R$#%^Be1XF/wG²[۬ >{ DK⯍~*%mU'vҧtׯK&`J3n|y Cc@0xX"Cc 3-1կ*r,&&)Nr\$2ovb;AxuDUlijkuXz%n|E<iG'\Jz͕j`f | ~+xv G WȢnh8;,W)0$eFn-A̽8|ͭ^P)>P[ #dfb^\rTCY'>k sFl=|9~hؕp6בBn?k!IȔ&;sIeC0 `)ѐE.4Qں!7HD􊑨#c/˻EB!FtrE7H(Nk뿥Ufa<'+9>D7Wxn3RNN ΗJyiLsX܆uĶ<9Jc_ℓ" Vdl ^$]<:D830A~V~Ь%1q` 4{ٗA$!"y 8#JjL{ͦꁫJdZs9꿇D'O1 ~Nk mz9+-DLڦR(*' )ye/\B٪ݤq::kKiw-ĸL_[~qB?߱uB:wL<(7e[) cpcW00٤KCKй*V@~ ZtF ME͟B& _nVRRMv4Uئ' e,np|2 }7,m]rѻF9*+=O=RЃ2PCXLphW uT5N߶bD-rdk4b[F1L /hL^2IdnpA^1z,w>n֣'zCI2ʼn4wʂ;y5h ~}j ,7Ra e%ZMBj7vE'KP}^7;1k۫})NؚAtԿY}oPRAiAKM\C.S;KHshjīH|vLuqzl>,N>د9e]g79_mKR:HEK`rҁrD`argr?kS"PW}XhvMxej$Li׺v%?F;*k/{n`]ʦ~ =x۩/稒1 tDef, 5zǥc ,4kEA^<ҖXvlHz[fgWBgj!)JJ ݞ\a\Jl0ytUԿU}J| #IWal*Gf\wJw-=m% +pY lB&"Ӕh gb+7 Q%Hg.-,2K{zgmg(w31ɎՀo@K|q.rn:κ{YV5/v~>PiQ@YbLx~>U B2upӬSǖbzߎeЅG~g`Qw$?$Nh\g PEvIn;@ɇ.@$aR/wkd,JfDy$f>{YoC64ư 9\2Zwh0d|D y|2šǹQe*q1$}u?0I b)!j'zp xnzUH$FV&KZ^pxk~]a0i`O}P18-[IUTo%F;4Iu7zةaWs^y6'ۦG]:` *}n#3BFUE.4|* }qOkI:_lE7I#7'=݇1+#Cݜ5h}bjCl"֠dQiK0)6jZ\6Y˦}'V'^t (^)%rK1 YÓj%OW?؊ٌ3ݤGϖ0_i/J`}+XNׯ}[p`1ًo]$`O z3#@|}c~6iDpxs,e| 4 |T Y"Sjܧ"ϿގefIOϞbzgd~R1ߘ[}zyS1do.J(n7bwWHh.^CLgrh?"a+_JTGQu'E"0\'O)|LW,ŹOyv yh!MAP\<C+YggƽSL'[5vgrav}> cʻ0# D=]ך9'{q[C\h!"\:mzQ 羪S;vJۼ3hR)g5XDЪaNd642}(īG0o :A][ע0z-vd Ѩw8/]Z"͟h:[N”C5&K'#{,讠 .h4d\笇vR*< tAe6/6E ?}y~a%fQU=ךR dBqe&ءϏrW.|m)W WRbdA`dxpcWZyNi_}7~ԇI1iPU0j̇2S,> g(HqB!J49O8CVJ كm0x45n'KtC<.Qxj$ZSCk0[^k'f.kΑ6 .sOM륵;a$R5_(m;y E gp|9&` bPH:Y1BSd= 6xޞfResED[O-g@mx9 ‘);'<軲AiKQA]&Vj=n]Z6P.1{шXwwHw(m^f=(QPCaΘE"l %TطX<vhG>n>u>ل T>41s%Dw_n3әn܇ o8@}LSˑ+{8?/$ESKdxMx;k/}꼣 3dE;;4$K-^ޠ&P9R3L⏪Oa9^Mun`=C+ b9Sy8 gqh mmZvh]''y÷[vJ/ŎVGfeU C7MT%r= v*# G*6͜z{äZu2#Z+WLbi"`X戟:I(kN5tiC0 #A]хD ;`|J "=6qrMt'.gd/AuW^EA5/#~fYA7v;>zbRSD -ΎhK͈ w#it=e+u!k\>=*K&7g)ltd5}Rl~2VCG:TAXW7y<yuFxiJTcjoz-𚔍-H B /^|@׎ Y޳F6QɔRY^ȊOS𲁮4#|6nCd= ,5!k^H23)Oe\?Ou \ceiV^뢤9 ~%HMx^7!L[\y5N]C>V(anVYmOta ?˔!Jל,J9Pr5`ֹ8B_= [Ob?Lޞkˀb%GK+/׷u|pΆO>Lki$UO t9?4֔DqWeL;C)E~>J=0צ%F7g@60e+&Ri;g] ܳSf.IU43XW2W{#9|Z(Me?s17ax>ϵVˮ}\^s8jFo$VZ8g;,?|%:Ŏ6Yۣ$op%+?H%Rq{A轐WH\PܗgaN0.Oܕ4f+۹K[puŎI9ڜ߮1'Ia|5-_ON4XbaB!&(ʰ|5꫕j±qs=OLC F?8\|K!XQ ",yֵ !'2c3U%0W]>y\{#8 u=WN` AڬwbJP|j&WxT.:o,/!s9ˡeޑ@XӪm1ƓY7"G 03jGoȭƋKe;my S`hwޱRXNl:BMZ:Pij]OgGJ4GGk]Ϟ<+$QknӐ6I%c5))B2~3kk͗smɌ(dzp+ n+~ X"&6 !p%1┗sܝ :JZ3w:(˸AC1魆n)Br$g]X4~4k'xw>+_Q~4^ێ.?0Bo4o.h{^8@"@1G n8ٽ]F<%HTp2(`9k=>1~PIT96~ɮYLܰ2" ާ:rwpQ0hvNҠFPaR儒p P[ rxnr`^:O0XbUF+Oؕ`Sc,g'DRc2$C#! mtbhnymq#ɦE(-R헠4Iן[o<<P"^fs*+#oYeTi5F\T妨Gd`\'sTD=5ѮRiВJjɒ[~g<"g(ٕrPSavA붵_pa{- 2_}<b\Y Y%=˶t4%3.~ؤ3{?v^6)K?\?]0kE%@_R4\ެ ZżE/O Gob-Fڕ4]Ȑg =töX,v6r |gF叔(І=&vx2(Ot`ѓj\vRVٴwdѣNڡ Sݟn @Qȟ)L*'9]yw"4ԃPS4C߆B x`"5#csNmP cE"٥xT }  yó"4x!3naXRH雋ⶂWqqg+m&lBgo9ZP hAwԍZ5H^UZy` C.?]ѽw}(d[>5ҕ[X-v"8=6g, xK+|U9Q<zM# $reWs8C,n!:p_.Ai^;MgYB!nvmJT+܁b_HㄣI٦6X;D9ܗZ~MHH Qh q(6H<УX P3tm9@gtq(Uu^P.8Qs;BBN/,b7 -&,2,t5T҇u^yˣ I-wXH0N*qvfl"`_WZ Nk*JEL2ho(L<4$"4'.~{Ux=IUH}5Ԟo!w"FGH?ws8t60dŚiD(6[IJC0ƪ;ZM;J޻NRA~jK֒I& 7vb^?@m}@X_OkRXEVi#!|*<'xOճ(4k#nڳ53,,x.DU qDk* k 3cTڙD7܅jfa}gZla('o 7yW75l.4e5M;z8 !(]xi*re woɮU' ׃j4.u8XÓ]v&<>U{VwKs'^om͟Qob8Ҟ|\8Կ$#P9sr^nJEnN T!b&[w2]V}9t@)i|f C!D "E^DI$35tnyxU4[tHp 2c 9X"جQ%MWY\7- NbJpއa|q21SPtxjlwұ I&~d4 ʌZ›44vKT pt] Abr!vpcJ~0fZ=04բ2S[(R>6 }RE7mu˥XծTm?4861`ĄX y=N.g׏PUɳfJWeKzt>؋O[؝Ļy.bd ,4 _H3Guk4x^%تjoF~EtC.VXwքN ?YߣGfI =RE؀ʌxDq½61i3T9ϡRtod[sŇ~FdrO{YIvlGsr`0Ĩ/QJ-.&G^_Oڡ^ Ǐ# wvD5h/~HB1(+zӯ k5SF6.\Sѣt ƲG;R݂@~w)D9-0=p(n}^@ B&b]~nޱq; %:LS{q|u`Qn@(z5۔ HQAdG73א0aKlOK)-w@L0I WH%s^@B"\$' /:w:׌.$];Ǡà'nʔpDŽ.mG;f_A(4A=n]y R])(\5OאL7 @#Lrn:8v9KW@7KkYĨGt?gkESJ.Ň$HǍcQU+kAUVI1|j|aQ(|P%؂#_fgPXxb V2:E'Վf.($+/a|!){\{K U+YHyXA7c꿷ФC1@r*gjoGzͦx\\ [sσA5L>_G60Np N07WpfWrsFN6ފo{im%Gy ,@t>rd>w>lK9՟ ǯ|kvRV<\% endstream endobj 875 0 obj << /Length1 1612 /Length2 18720 /Length3 0 /Length 19560 /Filter /FlateDecode >> stream xڬc%Yii۶m۶JvVڶJ۶y=xVD3bZ{*( ۹21p,m\edhL]ФN.v".\uS1 M wt4pP*SRSlin fjc`kjz)`fic WДP˩ML m F6KcS;gSJ ?9tL-n306uEp0utv t;ڹ큋=f"d7/ oVt0t'_7o?%bO.#S,EL LlL;U'_8X8ژA32i74?"igf`dL f/ C{;O4ߔ;DoEyoyOh1W9Cۿ cl _ᆶ6 no8.!hgW:-,=LM,]-f6;/_EL-#T,,i=]v&IHN/" !*Cw꿢j(E-x=L 9}_kYC'KߒUJ?`DMeC; ]נzC.sXebOh2 :6TEp6 kjsy Eu8ڋnC*חi0^.\=zQfTApwRQIo 2ح 8>,|pldx:'/0 ͍}[1<5wԅZsHTp8" 4}N?gХ\9i8?`Irv EJmFb˶qQvŲo$ܻm/xP`H˜µ=&ǿw H,#辜I^iהlRiۧ''rJ:wZ*LLJ1]`v4M 24)b뻳ſ\_(8Uo,G_q%{Sz-j{#ƿFLyHJb#-3Rߙ<`Qk“Ej|)>q(s!h z}4;(v_2z xɛ?pnwufZ,/׊ Hф/ߵ~/=6k/P?~08MG HlQr E~;Ɗ}ujWJTk0?CTP̹!V`@=@EtYJq*}%]~p@ISYݝ\Ħ uDIR8tSoppA=xM.=U tӟ1غ cmTfAhNy|r8O3+>Q5;YJWMnM/!LWmp*&v9`9%1zvcrޣnhxϚ)[6?_AF  w@dNG4'j?gciv#~#)KTgXJMDZf"x ǹpϼ'](c"1o^Z/{]36R=gyZH3Y;[ֹŎskd ,. "|Y3J:<)n<< ב$󣕘64P&֖6V.vN@۟IUZ4ܸ:qR 2N ?߰= G2_-Fr:0ck҂x!L1"/~6/OAi(®?L*g.ZnCHsk;^7(r G)RwMۿy1~Auy6_`!ƹۡtpzʴ=\.{>Ҥp5J:~fLr]XŻ %zzgĉiYf^04!ZyһV3F^o.=jG[p$7kaNޝ2a*`` /\w,x/Mj>DΒ4V_nڢ ~e*]-AUgNZ?(dp + ڭ#OeN{)MOۇ$[m Pns'32]ɯ0#La ,>sAE5>_%!k/lw o77 }Xb 7+zEFT~k#E'>uJ4f:G:` ,}^0*t\rN;Cm9ZwzVaC)NwM҅>EzmvkإuSM%v:5n~GyEG&?Jv.Gmͬ!aAFhF)1B\hqCVWhFyۉ&fALdqn3~HY@nv}KC|=zKy7,C:w4a'8σF~6TL&^I;h ny֝(I@RSӺYm)-$@ңAgǢiKla酥$@둪~pq;In]T%Qd(IM"6ֳS97ei(mtB>\-)Blw3+~2ftkVgӉJ5_ tIІ0!1.ȆUׅQc2D(+ƛR(D GRp뾩P?ݺY.mpW/mkwlVK`>TIn>D5nIw'Tw:c.N|אT"x3Xg^ա"_߈r0%x65Db_6/E Ѕ A߯'k"+M{/Sq,Dx$5mdFv;{];_@~"5^LSLt}̳L?FHK밦TD Y)ӥa}4*E5a&l$텧Wg FqYc{H gbqR' lg|Ѳm0~PQmwůA nn|fA1ɱ6ǻy&pV*+M8N su9@m'ГMSzl&hH~( X\\A/* w'4P zly%-p$a1mW ֵ^3oF Gàe:Dpx53 {ۆK<'"-1Դ'H8M0P171%OV,eQf;eqbtlF2 H;ʞPQN ~^?P.y{ΊR2e}TɑR>CFEu=>t"W}q:x_]H~!,bRj0v6Mp! co =OJXh~D^0vlX⛬@swQ?N&Loryt;;Eо,gv , `>^2SPk3 T֕hAdYq<#Mé_X('O9D5o]ƌ+FFTuM>H^}u/zhEBPhF ݟku[߂3c T Ea509gf7/؜]90vjpB谒ω^};ϟoi9hԹ}:ݏ8¢G yUt?&W/ElEӬC1qLM˹оU7nGY5Rw+LoW&iCK)4ZKP{pYE7iA^X|C+c ԅ$#4q[l*I'???a) }Kf.29) $]($.0DO5?&܎r)}Ks]Q b_b} J\ebVHǚ+-nOO4+A$ =@@{FƆZ}Hb{s}@>__'w?k>r.b7%EI]Gn:[[5&u}0~lCV qQV5RXIј$`#a -5梸դ@;zAINB$nءy~%@Ei64nlL6~M?Dxn|t%;|?:œJQg]D9M48r],Yك U{h#Ce.0sl|GNquSɡIjGj?TcӴ d؄2rԌY,Xꝁ5<ݕFMnp}8^J$)> 327BVi)cqJfKR2E4lפ<}{yV a#cЕs>E2S@',C+(T<MW2#&u d4Czr4)uX :'>j1/b˃>dNE/Pd [!kDXY8۟wg(/5'Wػ$?8n#dEb(K<<}7ֆhWgcDK7U3+tISEQעkƔIÒ7Ysf:hN!U ^ 3R& SP0ôdUMGj_gu*9t;99U+l˚ {V ):a )Pti~}_Ï9xx25Q'w;Z@,uB v}S0PŶ+:$BzM  0ܘzL̚ٹ)Vkzx)>}:s:jruU(oD%V)&Z;);lsf#h}Ly V}\gjDw- ]Q&ĮTzXnZ;JTgt㢧Щp!`x) 2;QdM7r|e/T@y)x/0mk+OwK;V#3/E, }GzELCA3h996{KnU9t@9#Dy~*U0h(I^Uސȳ}Lj8VuM ľkj:rEr鵡4v & =E"8w{Uj/JTHݍmeٕ|о(aizo~wn3 ˉlSt1^7]wN/#舲h3H@c4mZ(\B,FCQ*4~ˏQzSr 3; U֖XĹ1Y'ʥkذ6vgJ5Ak2곇bO"_8z#p}Aq)z`b-T50{o43$xαՊ̾.Ŝ]IHKIy#,7ʔR8 M]%5BL+_r.A(TCѶX@MUf\,F튽M#i.06ρmUN,Jz}R~_q~#,GU1>ziwX<:";,ZHCqBI`Ag)L%z|cr'.kr=eIi*CiVSb~=?2hH5O)cN9MJתlƨHa=luA|&5}JuU'׷C(e@\y'e:I1a C IIjV~)l_ҕA%Һ[]`8{J{[66s8vEW0 m I)RvC_[g6~T1͛{ ko\9;њ0) S8=cu |:qfzRZ|;d7\f$۸u~̬P-/"K$(6&!;dgT!ЦB:Ç \?^ɩ~XM9vicz1t1ژv9,}&ۏB5aPxjxh2N>Y;*Tu/+@ +F0R/RNG )yWA6FaRFszaR9 bAHdys >^_ULlLPsύ{2x;5KYp$js\crȄ$zg ؁y;FΕ_ RϺnzKADJ-߭j_+v8d9SeјR{p xh灸=uSB;.zOì.Fz1_|iNoaFc:m~N}呏[嵐6&A'1,0NLZF1}&Re̞vV8 G['G[5pޔi3B<^tZ Œ1_OкFT$Cz^l!b'N*+9և6^  /zfr?TA 0Mz; {[YK-.(cRf$F+]sR|23oRAƊ Y9>p2#[Q ":n!u US-0M聆fЏ B;%7_*U=BL)%t~Iː8zq+;EjgD 1LV^NW3l%J?KV,R8.uԯ1LF))](e6,QFu}^vqV0?j=r*Kd[ Mϑh:5o^r+e c ,}9K9UEf$"FdϿ3_{RWE*f8ī`CM߶r5^ffl=ca3v쭕J VUb;KDpIEZRq0='qy5pqVa [eT*qJų[rmˍp> p7XpAVV-Ov $8 I_{n iO՜5y=UEarwHX5k9h-p\"%"29A~8~nU%QeJ#TW#[3pkNҪlGA-zE8oO%j:Hb{#wavfw0SBs?HIpqX}ƚxk0r\[H0Q5w3C+^$ zw~!*%_3G"v|soZSO ?eDLOs~% ]}L g/݉%U,:‹IsD@pԖ!vQfC<&ӓ% L-ԶS#yZCnH}`$X}taRJ&-:hn"oeF{Wy7OY!LV%jǶn, 3@N XO)$.׈xYgMI5hI넠Q*D%[j t:<,#[eHQ!N:!*y̿:LggSBZ6Hx!TiyE9KdjJUY؄WSsۀ{xH 4\DcWQՠ;@B6Mpҙxw]9;ܭeds:K C@- ӯ,ɂ$ΧvwZ4gϵ^qF#pOP7hɪk3L5yXOY[xs4(%[ع;hmNچ~e5eAH"Q,#hZc H3z%Z *֎.* 2OuJUVE81Y O;X j&j껸;+5Hpȉ {N}7eQn/"_bq~P؆6;h:@O,#w2J=;W yk؁֟uEr SGDk {UsX a3X^S=;Z(k`_#&'T \QkHwj>X6[-9 vj7Ohfs=nfbXa{4>Uk\m _mJ8'@ wChhevϲ !ErUED\UM/MΡ{ʬ3hxrJ 乆X%x\}NďQC0ո/i\HSWj^tm--rniy̦x3aYؒxX :ΫBwUpqX6r KSxkZc]ir=[m2,3ѶeJawi#lD$߈VW71ecN%(5Ha@S"RG詖CTe5!ΰ0ȉ~iL=e'㩨/$f"[YC]p96dzd@nYE }ɘnAc46qO I ]م2wCrdt3"pHʏ|KF ^ݑA'`&y}?rtwdY~yJaK b<~ .U! Ψ1vCdqAR{7*ٍ=ҕzoa㐖{ }u%'~=^JޢG|jdf?H;bS&@ Cڱ+cRXbg֖w IZY%5lka39PkLFpM)T  b?@1W~YTd!dka^Ӄ 7?3.5Zɯ#AFP}Zl #F-@d`FXߧ+ֳ+2g.Akx h:𓴜[C!ѬRGe%aI:rqR`/Cܴ=B.D<.v|0HɆ}FZ:0ġ7YQjs?\yd)qU$ox!/mBxW^SuNU8PeuJm w1UH } 4~jyYG3<'3v2 fWNXű*T0ifF1E,P %[\‰~Y-=Rhbr8Ev>RM78%jǓdaScq.|Eн>Oݘ|Z;[ͲQyR΁޺"җ^r&J߈(bS08BWCCɈ6^΋#+ wtߑۿx|KAD(6]SNEH|ud+vD4%ė]|Hu-׃ Qc'OR^&'qU*&ѝewg jRPD,:-oDfAPxs̬I0r&uĊ:ŵECCq@|m>qvDڦk-]^)*,}^"^v#d6[H2HV6/b-;o#^V fNMпgt،d D0ctnЂTkp_:/tV%#r p+)*SxnAnG-W5}jD}Mך0uP W@9RPl!F ZT CYtU&+4b[lb{?ח|"\bgYc^!uzw0)[B7# }.;N6؅LK<`x]Pw}]Hln)œR3뇇G-{Gl]328`! B4,,Wɐ扢)fd6vG&C. w)Ώh}.zީ8/C 0.=lWZ)1VP0Vop{Pu u3Lo-+˟paHf-~F*Vs-~eC_ۢ4DեF>kz\v foԗqXEt`D=Ds 7)",p99{:G,juN&>֡Ǵ_w1~C^_as@$z?xa5wT-_)t΢gGo2?t"bնe^_mi8uMc{B"㌚ oNeZrǰ}3!+K޽؁7zEZ@+p4a@"@*vW@/ &*FQ9a'%o`,o"42u%lA083RR^7gcqOv,qoou׺a؎a%k ×s`j5wV#%j, soVDҋMXQCk\j璒 ⏘BZ\}mfCG8IFR#bxgӝR\%lB#m9),v<L#L17Hnv͆ȉZdv/rXx;nH\O-bh֎&^}]<ĐM?qWB8-4˅ȡH&ZbF`4'#Cya֔ \35|0 SK]@Ϋ0S23O&s wd'S;;;fj5<>/_&q#ԒEn{$?=8"⋡u, bV۰N7>BPꌛmU@tD* l=$ \$ۦ_/8;Wd}DUbS ^Mk7u]`iE%L{h/xYWF>Ns@wb2^ؗF^hK)|q9|,h ]*m*LOPN[TrGM8Fe$;m6z-[m|vfhn?^3ֱڜPÃ8 Z61z}0Tyƒ!= "Hl\ p=_Rl8g|4LIர2m?D;;- 푯C2$Q1 6fW0$ND9*IIr$꟮Lj@ 9Q0^Ias:Յn(mޒt!tZ2o5+;W{aFǾY]8e4G:`K4Mw1 'c|J4U>ڗX&%4$`/w}`{ڀk`J [`g/@yw/mn՝U '[+):/4P94_ʸ$:O!),j'#@Jz&8Y"L62yҋ֎Lf锆bvۺ0opjrr*\jŰNJ6U)}Jam[oD T潃$ .dN`xh jkls7jqolDTgV  ay'YDf*{wǧKgڦu->Kh緘X{Ô&Xͼ>e1AV&vi OIc~PU`~.OhW1YMDt/ƅ07Ȉ +\u7y4;!Xەtx:b8RL/쩛V@瑧>lz4eP71Z5$.GaX~Ef31r)JBF8jJ?Q-PW(V%5`hL&2p߅ٴZI:'5dhDRf7n7'h+\]Tl7cVr%BcE:}@'yC{sMi{>(>[Ɣ4MIaTyaڥWf'9{VÏ@L*%pMf2j8B,SRS_AXxY"Jb\=g^"%hG7t*n$;AHߠ"qr&DC<85tgG qJa l=cwŷE@$SQ{eF؋6Q ,]D\72}}wsuC&祝ހe>O6xirРMb4{_Rb<D Am_ݦ*wo8r^<$y 3 u5|'d bǴs!R1_.ĹPEfxՕ*:UѠ׭ٔv."-[p|"̍v4{}78Dnh\Ie=2eH X"ۡ`¦{bvk#l]'(o=#pwBv%Խq.m]/$юTh5Ib,S&{r)fOQ$7h?Uu1 G^]z8ǜ9fL& Xk=`YWu 'k4$ v2|H4&v?wx'+LîI<u)i0  ևK2夎`@f^S(rj/{ {1"'gQ8l0R!\C +yʝ-34e \ty`Ւ ?:N: !JŰi2fP#n@} g+qbd*^`=̀fȦχm*E!i;o1*4_ S<ٴ)NaXb^"(seATt7\BRY+o$y ;U@qeQVOBGuY" ٝD{%;ݦd(N]"j&c/^*v%xeSGZn-*<l9<ӉL!Mc>Ax6>e_/A׼v;5G6i(rmsֳVq̢Ä5O)y}GN'OW/ W_L{$$/a8Ep;,@j|b30`SbmM\Ƽy1IKMB}zN|%>%>>qGnB^)9SlPaՑ4uDAADוg 8jO++A\\JJan^j!q,(J -h.)|b@*a[UU!D.4ۛVft~sYʊ*3CM~ft/W6׾s^б;HH Px=nJDJ-X _õ7PZrKʴ7idN9z)wMuc|)JB<8yx͑Q7qVSf713Qq{;t7$1Gm*O)c}J86eˋ[K Wš@ R33} 94gXjPP͘9٧(js*g~e44^ʸf|Д<'`/W/ݲ+uH_'ݞT]]tø{@e!%&}H),XKcۘiz}ĦֵVM=$X4߽b4D&SORƄf~nB<̒-7\;ſ$qlFO 8tw fV'%7W$kfn!m)(OLv l6Xe C|r&zŜeY4Nhk+V⥀'Uy^bZ =.uo8-=ĊYkTU P1` \}3a)@Lpf ,>AIzMft~^,VtOӹ%JN0$~j]+> stream xڭcx]%Ul;;mFɎm*6+mv*}|s1"'VR67J۹330,m]U殊6;,9NĀ&3777,9@@IMKK_\ƞt4Pp;\R_ UTҖVPI*$v@'#VLr&@;g 5 `֜r ;@˿a@?dk` 0w2s;{? 'dJ.&N.Y$]?-{&//_p'1`j`c7_2'lig_FN6@g4 ߺ7rpWKg,3ߜ&.s[2,vffM]s:k@T "Ll<@3XF{)Tw*3ω? #5.}66 Fߏ +c9yk]0#[KCwaiC3+ =3 ӿ͖@S%K ߙˮng t_c05 KkD`73_3I^y*5O є7?<""ozf= +' /:8Yz31011NzFQu13li6qurߦ=&K&Vi.5X9CbzJ TwpW14L|z.:|`Pv'/}I{Q6)9iK4#!t84w'U! Y@'{t@3I@nA)8=H8~z{GCktJit_oՍ}Wr8YbK ѕp7Phg~6yڬKj&hQ%FꩾIgfNkʢt(XDx3B p#dA1f㚔{q8x>&7H*dQ.?cfxxT8van66BAhw:ݘ-IHS/ Wj]t/hu!si<1G|FݞƑ9%E.9V-KXzB4֫Ίa+ޜrO;&<o ˅ؚ$WF*qן5B_Űvc G_z_lGt ]7A0ݫ\vA+F܁i4lw >zk`c%rYeuMI(%Q&Yg_:V"a]k]W 8fTa@3D (U%tBQ֡ WZ>w HH7%|h9nы+%r:E5OG|O:) =^A E Z961!YHU:Am5b [Zy#"zT5^?IHDkȥ=2R2⺣Bܮ;”chdqdsB=V?&X5\,?Nqh\Ɲ:L5hpmL&4XǬw> HV5h ӕ9sZL eaj:x&<+9;)A6KRLޕ OB ^[p7 CCN<|Wy!i\*\ fx1^SތBbW=qJJk&k@Fa3fX"T͙DI̬&j!e=93p-~3*Sc=_@:RwD&T/ JCև_g| ,hM9iYxbU%K2V*uHyhVzRzݵ@ >FA .}_屮"AWeGMpء$csUtDVB[u\J띍Ex>EyW y*z6UwhuDdҵjT}E0b|iW,As}.N4w<5* k5A-?!zBv20ok:aKVn'f̑96Rj/Xbuz؉mיܑi_4:K9Mxsbs,1k, _ƢÛk""fj!Vi1.Jy/tWMe`h9j]o2=>Jv)mHҟaGd1@O)cRDŽJ$3ŌIߋR8W >tca=-²O ޚx6B J*>9IE?,) FOTVq8<>Sタf~r`|#qj_*97}8)Ed9t{^d%JoB8ѼY? ƚYsS⑞\]ͰK^/}GW?ߕݑ䘠wtJfInW&ƐФH)]ʦ֔28h'lk;tl NAuesgyy-:"\zjpԊ'2Z6[(XZ֣mA"eI JFB4C!&&\ߑ˜Y:F&>_£~AnInz̏Z#Z2}2ϔVpk}Mu({Jt* o?R_/ϟQxBi9MoJz'hXE|B1E<a5/Ǫ,(OnWwrAQ ?=1UKqnʡV^t%p`/y~\x&j7[D$W|OWn 34چdȮkՒIPްȲ`lT{Uɹ - SG+>Ȕ;BaBvt297 H %k]|xJMj`@Lb!q5?Ei"jDc*D^ҢNR:V^<ث'XMM$#ϮxǠcKɽ(̇X qv(`nq{cFnAA3䎊 }!A${'43v't/͏W<3v[.#,$Bn~䅃U%Zp$iH^hŖ|-MFS9$Z"o7q6 &: X*|Ff^q=bDDwcz;@cxa8g1w8,Ffm Lria%:y:nAnœt9{QNGB& i;8GܶBX~f 4>Z;KLJ_er_\4[?{,\ ~C)5ҫQlS&Me3 U/b!Ic U(DvbOp g+nis钾?A=J]w$1ȸ^Mr)Ւ)$L0#x\w}c8MWKutN}FqMK ]GmZn* (xB`qs-]|Vc| 2f0}wBB˗)rPU){;3TkӶ!L~~#|J׺"ɀ8=9bF] _:< ߽eX!Zc-i mc dgHAszėg۳`+hs#2NH?x^hD[ "Z|%y !\܁]uo8}awŊ hc=IbГ"RWF3UZC=Um}PҞ_ٌFMkj#:&++BKXV6ߑk`e0p!|;,@t߇BK $XjSJm4 BuV<<hqxT !u4M*b| htIk_N#c7)8vu->,pݫ%pYd@̕.;$,9q@}BSڜЎz0Jt9LP >7iܫ19DxbAhCݰ;{R重IfnP]wm䧟J_8@qa-Jl|7< !:׊|Hn?Mx+ /لu‰:1yܬgb嗋m>ݶDP\ |=,@ޯ^.{=bu ,MhZt^EyPnhۧJ߫ 4~BHHW 1Wj,\nF< v{W*K6dX8pDŽxN\Y->!lw+H$^_+ˡL.vkeXBb!+/w[О gJ}$ڹZrB}RIrN}aeH?cMJAJNïCwC;Pܼ`;Kn2ĄMÆ$p;B%~m[-H72 buz7]tlݜi#O7"XDHn,a|=RB/Mtm;`G5 lf7.4ϻKP$P^vec׿AX!Af>hvȺ71&4z!Qs'Uȱs/gk;?-ke\ V(7[%Jf|7ݭN0WIWSWgmM^:K_ZO01!iJa+Z)\#ڠ M"=pY{\,E,-g)4UwR woEn8Zs)ө.Dj 'v A]703B? ,wKi7S%h${tՃ >i' ;61X/A}F.dkhJ51&e<'[{yVPOU[t%NFH8ʄzjpc=Y =ޑ8[X4c.8pPP'G7A p)9P 0WrԗwPʳ1BJk^՞埅ϵKŖI% ?:%6n\vgS.LZ=U?Ik|,5]g=Wޯ_?mDiԶCzsGvs]v_t'=+g74eXTb="K?j(VۻÄ &aaS`gEMDqԘ\sHa䁕[55br.9AqN"m9mø˙T_rDKZN ź ~gUe0问"gWUg?t^SY"DG~I_p;Y'orZ֥;K~&[4,7 /wu!r8{5r/4rF4!׌F]Lc̓6E=#rYԨmY=WwҔ鞲StfPiZj,R1,aUw2W,$а˩rp1lmٻK6\D9qH*?Z>x=C,?u;N*'8oP"V}7n]WLVu͌JSV T~EQL]6;]˜QOon_JH?[GA;Igց_]h4͘m6`942hg] Ty AGNyH\wnq)kY/;#Sۛv^:7Ԋ10]hĄW(DPAoB47OŲ"j3@tOXY(=4 cLȤc28~LC]bfiMSy@fB c㉫+^mŘ튗-pPQ: n^<4T:؆< ޤ,&.)eفn,M#CBb4qNh 2OeVlh:|Z+;ԡКA^/[ mي;u3(Gr:Ow>#_eum "'ȟ1?$YՏ֭P=&zx)ѮbIG+Nq?ңR@oK֚|g-YBvQ@Π';\ݖRg( m=VzRP1Ӏ=6 ߙMm)^/zk̤P+ U4U^5R(ƃ6Y1>ܭv 75Фڮ0ҚҢg5 &>rkT;$ }έ ;GH$Q~FׅYJ\Q 5+ج!vi-5%SlcfmWRp^ I]K]}^> -T3T̟]?FBs $H \56ъ"VkN߱ -(iڻ(VPѸ'!z֪*GnZ lY> !ەi g kqT!FG.fh@@PN0]dŷ(VHr_'ltosBz>'>B9<mq;0Xx &jX77`q3̮_(h/? =v 2R8ji:g1 J-,kL?Wbw0EHHO Z< Z0%E]geטsY )'b)U\7QH E wqUMƑcj2akU !Ӻ#c }Y*=Pz'~ )/l,HER+E%+6W8y ^J [=`Rm˴~ʌ};')7ku­kt+& C/=F$\P26P۠'O/発(ch y E|!f I D)p5ֵ*ͧ ' FEb{ bhbJ]C]\;WM%TY6+? y@Ƭ^2w9Uo" k2]jH7 vָnw @eOܥvXjArtLJ.Q,UO*<΄oG; ~sAj_II>ܞT<4Γ_]kp5B_wd=zueX H|<;WdU^%x'Q|U?y/lu*;܊Yd\HV a%\ɳ6|E&VNhxniL&wPjTG%8?̵*%ziag&e]p (hi(aU(j+}MR|ɒ+f>R%ZMs{vT#9WU\- 3eq*ЅuNb&JCy<x2&;zPP=ry>;"1rܿԱPsC})/Jwc뷐 ?rZBGj U!|SmIꓻjYFlo|ᣑ',o^KG6[&ו01)s~4T9"R@nvqs-<\pXrdKiB&ɣc!5_ZW'F /B=J([Z!eOr^}][IJ Dy62"s! ;J w:q$V>M"@v:w\qk R"IV&u>b@aWģmlf"Mh8ch\]iڏ(1eH3_ֈ1C] Vk0iZGAoQ38҇9cг'LM_r2 &塅s H$^il"K? 游;286+B)@ @sndv%M/UW2 Eܔ۽l灟rܚHUH#O! Y*ifZ+;LVPl׈yl~I_@ퟄyUwa=ج`.x;Vl f}xbŅ1%(XHؘ 8l!5'2G)6n.3i*LNfz7 E*B<8[m!9=":{s76SrgP5q/Wab4kdޒih-vW^[z75cnX\vB> jڮ{XPgYYXIRt08gĵxGuHeM Ur]13(,xy:DE<@o Y|eDs[*yϧ6෇tߍ|$K4CMlH<$0*؛GMhx'4f/rń,mO6m?Fb-O:~.L|/:̈́Ij>[ aFYc+@, 6VjfwWAƷDܞ|nr1x܌'_iؾj ^;}rj_Jn|ΕHoLjTKU% WB>G޷-$W#EBݗYE*53/#āz`ܻ5Zĉ^Y>v[7!CM LhX˿|߬rKV Jw (E:emEDGD,|L eJu,! /BOHCC>u_% ;/^Kԣ'^RS~JrMO彵0ٮ) g/ AJf?أ[D$ apn"+-*<@pӖ"w_[U*Tڹw3%^B$\i+$}ҺBf`nLh[`Uq B|jӂk]";v&~=!P 'rXw& H{4GD%QB?rWٞ~sL8>XƩ;4э61xqgx]|!##~P|~> stream xڭweT]ے5,h8wBp]u~15kVZkMA ljg ub`ad(lFr "v֦w## dg+fhMb@++ jg2pPhW?=; s[ hި ,35 - TPHmF%gck @difv,&vJ3c F= fE:ڀw 0w4uzdkblw߄#l}`Jv`'# UIL<, vfv&dnN2LA`{k#`i8Ad@p9Zww쿺:z#{{kw_@N`# {N [$E\7y'adjgk0!1)9PTfo"E; F6 ~Àr/d?Dk& 'i'6ۚK#,r*L,fF=ۮnk tߵYLlj:'.2oL::rtz;Caع<X l\ ?ky#'G@hfK+53kNTlMG M뿇t -ۙY&8U}d/UM.5| fymr;ٕĵHyt~Xl `Zry>+ɬ5xƏ%GM,N+FfU1eU@_%l.!]f,"GGN7&/ < c&݃pȝ0{„Bgl&>2XAmCuCzWU|bc.D:s#: B8^{pS hshGivD^6gd"Dsu IdfOW ;}IAdt #`['qiTY[qw eSXYO/Rk1m8$_eNz Q6++΍wmY…|(a]ςv+(5d}Mn +IJ!*lKn 㽜"6 YVrfA԰^CRU /Fi9٫)?28Yjs^[kHPGVH?q=$dwW8}Zzlp%#F&o}[G:Xˋ`<.'f`.Nz+"OBS!tK1-o,4KA|7>c#r;tٮqiX,ԀOk1ɡ#>1<죓"_zE?)&aEN? 5JGMJ2VuƎG`'ٯȪGĂJkܣK@aiPj]}!W7Ke+v1ΉD+o.2-;S$>ƭ9sĮTP@T7M1fU_͵r=13/Y`MTuƓmԞß#d2B߳|4[~:%Ii̚(Òfx$ B!e}RႄѲ +< N:J>ϯ^ٞ 8b[0P˦YxhK43 5q' Tj}Yp@ \7ZUTݶ H8[ 9jMNbGMẻmڡF Koeq%"(-eEAV(M VQu'{c{Q[ލqxaV⭋vT[VzsfXT.!HS0a+A|c:~HNˣr#Ͷ`7`4%sy*|cҟ5W4>BDiWAhu4ЎZSr8BkLCmέ.֒.V: @L9%h^tu?dBS!{%KsqnWFD_[I%xvG[!*%すq?yBr,8{O.T3l!ft3x,!c gC(Xcl(c>Y"x%R:( ]~hqM|H}{M*U։ QPD߾t=gLY-#xQj) eCϗͦk0Ensʍu_ nacx2V|:r*RVi!h_Mpb5ŠpfDT[QƘ¾6TQRLR_WZ.'Uie-O 8}pv$wrrr-"kFC]˻Yr4!(uZexvR''[˭l"N̂傿uy)5,K 1'ܚLh'*g9A>n ^WTk,a1*űOt%TlW#]}". w#pι)aΤz8'VyTӾv;d/ t}a&AFg^"sQ6!H>컀^N9˛ñ1lsơj`69ORU1f3b*l|o$z{wդS+W{Ckp+ahrP-!s(os_HR9e·+bGŒNb+q*ʨQXQh_xu?1`*3h~Bs/S~M䐤3CmrTN]E5Kޙ~hVa09q]cvah&[Z j4[Ϊp] u7ҩHmʩ9oX\i5(2QL fR8B>MJ-XOM w fVWh8ڢ grp11eW\̣#W7sU.RB1/H$?CRdJyzFܿM{d=C^}i^nP̻X!CN< aW6 tMRJlݦԟyFvNlE\`?A㤐KY]\_fެ\B]͙ 63Z;Bf۾R1l{Ȇ*^Q2̚ύ4kJtn Dqn.ϲEJ%x'ßc\V1>m>o8k,-nz|x"MIû6AUj[n ;ŦH{,?Vg`h]0aU,*qV÷nǿ*iz~bȁ}F_S!t5XT;+n=_U|DXmՌƞ6]g }a:v l^R*?D*QY#XދϱOi멦-X'!_扝/DF\G7YO\(Rt hOU4 H(1TG.%ng(oܟ}TMGar"$~UI gfH{x)9҆"&wR!˶[傱ۜ3(EqE3)pL{FP!/- !8~0]lб 27lm g\3t==iSi"Ia^(9d'|='t=l#xGD9M[VLBn *px16 "~#wv熄_E1P`mFsoh'Sj֕GcA_n8 ZwX];wċW Vbf.ID Q8V~=cܥF0jjlo1֕; ;vJcDCx!ɑΉ9^p ^aCSf}/E%̱ǥݎ'z RKe녮4{YIeX6%x*kVnCu7_h8!dK_Fg.wrH|6$3|UHK>7jJ0mRS,__h;@ڣZw%۬§'e@v\Fsu> zդZsQ}5c-uSwq/< 5Ӣ2 O4@M6|v \р nnH>hO'jdJWnAz<΁ǣqdF/O+#8t^mEҾ*uD4| $TOqla=7 lt $6"?(+N:p@ɲzNU 7fD y[Bp3s3'(GQO~ lH?S<=YK;2Q j.1W:W6Dm%Ɇ#>Bum - QQL8X[(YauLTDH7?5VNIFzJܚ\IPɸrLx ,[o|m뒯(4ٵV͒+uj~.6qߐH4vÏӥE_qLgw{;/*|< Sc{͸iZsENSF ߞs,\BQ).ԋ AH ;j7X4Fv+!\@H+AAuJW_AxC(9үDs&3hqRvA>|˙Q0G)$iX"8j2FGK> >4׌Y5F¥ w\[~r@|9i_֕NKt:W"=m/*yŔ(T¡ \6efB}J8H~jCO6$~=)Wf:Hq'{֣`xX6N^1'G0Ԅ?Swf Ûų;BZAAހă*ka.jӼ5%~Qr%"zzH}d2ybn™y8fA4ɵ"ZH,g X+ ey6 WFX#a$ B)qc鏫drlZbvluotMn.`գY}({R7Uo]f+2t "KX31u0N}D qf?sdAܡ1 PHt $uIMSel!uHT::;]N'PWRqK*1@L!ٳ ȯ|~dn|Uf%Q cUY0g|j2a,bפ wLk=q&ٍ p0.:LU*mLRr6gcpP0H:f%8'sE(Edlq۶=oN*pvWr{G=3Fjj؝_?йn,$$w)$zzzVOD*F3P!7u XƘm0Mv^ DICK5M'unI4AuOv0 y$:{H-'`xgj۾ڃ )bڲ;4TAX$ .N@z[WX0\*a$ <79SV$$[d%Tƙwˉ`Ҧ c9{Ar!LQޠ0\W5G7|IT@A|p:O-Qex>g v_31A΀;;Є<koc7F.p29ъ ¨ ~l۲!_*s-NTa聾 MnC(PnR6vŅ#h5{,') Qu\Y $nUBRٳԎ^!O!yLQlXF@<^ʸݴ3,?OGeN5hlC;P ΝZ&čp=VP~oS~ٝOyYb3dp{^ V QVM?`.X#k2P~oQa8^[7zXϖȷ1ϒh0E ^o+CHtQOL1)x>|T$w Q(35h]' iS-*m(~L%3)\{hSMr4w>5bO[ap,(&H*칋/i#A՝Z$ߢ0ulA_T/vnޢ-mef_3e6i.ݞ,p:v6nߟu+ )݂m"pHM3'n,O<%W ƹt$lgs EjbRhLrLD Ir0;rnzvhLjEqNk>A.JШ֠6ƽϞa|m|fTpS a3sY%Ү.DvDd*~5QFM0x[ژQUB# J*Vy.l?ͤgwL9/6.28CU=w=z(yBUc@E6,t=!\OͦFxS~pW oXn?eJS@ d*~stF%7>yrq!dB}۵QځVcݠ3c 둷"#cm)kSհ3!KQQ )239EP}DcMI|?Ѝ?Fx{S!#k" fg@c9M~!zjЮ%k;I~ D-iu9qqdꁔ yȹM==yӫ];2!j7A& z&3i5Nj%'gA#5] $UKÁٱ<1O#ۛ(55}=MY2+9Dz8'"C ]yNy8_01Ky+SRK= KW3zRKHv;-<Ɉՠ"dZz4.H*S'T견 "~+&{eYg['C$6$cX y,0U}:FAܮ7?ͷ~Ja C]l z'QDN1d~:B7F`I,OʛBY OyOnR0'x*09XGOS/O`e %󣧗BxNBdB(|h3)pcu~5aVMW6 ' >igS1 ߁3FqpOp&l%^Rl06-&}Vi|E]AYÄ||s9{-̶ְ .3G[窿r٣Ex9 z$M:3d @𘹿Kko ]QվUqRbDDe/ q U쑲z)]쀊 1)'@\"Vk ҃!`*49~6%R` ]L~86,}{)e{huDx2F|Fp|` φh.@8(dId٤DFB= ȞkËPct.ra"6yy6~VmH&,%<,YZO*jTS CJ_7!L@Ͷ#,Ba:c+^/9kGgpځAMɶfP]/q_q6^JKs,C} 'jwWrV+%Gd# }M"V9?nA‰Ẇ)DzV/{{ 7R$DCĶ^Mio@T}<&8{thvO4^J .]LJu9^B֋_Û~t$ ӴiVx mY;xO@EWq1 3*'X=j Pv cZk\e3>Mۉ5&l("MO%XҥfW$zr;^Æl1YrRӭ;X Vۻ]"k?h]z/E|(PNE[]čf}Z$'S63ƁnvOW>..E<+b7vZyyNdɟ1Xm938v܊Z80E]6 Lq@t$Qa  Q<;Z8ִvB]@]epU#r?VjNR endstream endobj 881 0 obj << /Length1 1625 /Length2 5092 /Length3 0 /Length 5899 /Filter /FlateDecode >> stream xڭWgXSkEDN;! !HH A#]z("ҫEHRsg<ίcg]][Oel&A9C5QHP\R`ww˜b(D BmR>>54 ,T`ԡ`x]R> bVB""I.Uξ@8 xC(w( ͠P p#5#cC-@ I{9#`> EbBejq< `<`8  DP; Bb5p$ /wAk1<1 ŀp,X]8 o P.xM uo OG 8Bb/}9C8Ɠy"0b0x-=+[Ø7nQRo nYuxèE@B=φ,oY p(!ضߝ_$\[61u(>#fm&= .ANE@@SS͟yCg۵UV| -X_'a=GJ.;9b 5@,~R{hMUu~­hp ϺRiE+>f4don$z*cy4f-t{e*ReV1| (qeȭџHG G=7kkd1BP8 ffbq},MG|@W>7k5WG)>, =TBqd.]+rWtzi EHRBJS~Ri!tr>i923܂fxXq^c.dތ_NP\íYJWOhl-0`Uf UsĹ6ZB_J6ftozm 4e֤畁q|9)״}lST)uc;Ked$)5?G uF[B78B.}II7ZDOU?o)_}2޷#|Ֆv jNfDL% SʓAp(ݘI!et5ǧj zyԫ-q'-5`vCBeXFPwrwj`&օv9E"TKhRePWN &ϴ9b)H ryTf8*YMqJꃅl-tQYDLOԚb_ Y=ׄ? ',Q[ - QP[ Avt! 3\[j5@ԃ6gJk\jOmH_K^0lR Eɉ_ͮsjnh8=hMOja/U0^Qv9 &]^y`;gtE[HQ>wXVFXús~+۫I6ca1"4ֳ\#]1{!S5y"G7(0T# !C,uC.q$E&ɟ~frͻ1s9xⓛxܣ<_F ҷUFka:ZW&;[U//#[%]t к8^=sM)@nDMm(q"=Δ C (i5%uC;6_Ky;%1?eNwCϾE{2ڪ{g?aj ߴFełh'",s`P[!kl7]X|ȮKpkg:mr$P25r]~-iKb[A#4ARDG?7"EanR P*g5m'ph˩bCL!I?M_ҽÄ~<țc1ȧÖ]UYmKzLwOg>pZl3J8~IMg3["9c!١EE^"Vswf/ʖ¼VWט>q)QjJ֊ً 7s3~QWsɫ-5v_L7MnM¦W#wc@H~eT"9i)IcSn*n dAjcg4eԗd͉?qR#}2lڷg?idĬ'sZOIkYȡc )}D$1+Kc@h@ OI@fUjZEײn_9 n 52u t}l,Bۓ.^Pϗ(7dV,9ϘsM|~mlk+⦕DC,6;5<_,2L.rƽ@/q ҉/s{($=izo  c:̡/^{jWX=AԌhq~e.eh:EY-=vTrG3SE+ԈH23r% 8ߘ~Pɠ9pM"c-3O"m`iBF(X(wb~۶>&~s.Jr dwX$=p=ڳ?S~|[_>@ |E9=# &3jgЅq*J%>z]_l?;XIY㯔/l:bʏYTSpGb >U=I#62 AA9Qש4ZtQ8œ3Qӗ#)|p&i͢X)G>M-"W;a'*y֒?|S_;N}bްC?@|`M *f aY3F/ZbtJo\,( aY.c\XU (n1࿁<=[9&%SM_7^pC]VT9Y-M"*>;Ij(m:jRrϯj1B3p8"FU.8VrD?sD5(;TQ' Lfҹ49:9&k0grX%JQˬB?T♭/tZ] ƄwjwC&1F~z_%`LYt&;-#Pyn؂g(= (,Mkɣc}m._;tox68=2$(ߡCQZ?!F;wkY’˖Kqglb| V}U Y!EɻOo:Pq$j/Rߞn|QPI͠WڄLIܜY n Vwq ǭ Wj諗,zqkѻUB/jm+ysd^I]$p1aʨ>5Ӈa/lD ,\6:UWko{D>nc .lW܃֛M\-[$s>Vd;!qf'$±JX.aOFGz$ %"5Q‹>)˓b\:wS4N{L $C'aܚJT4x_= ؾkcaQu07]ѮB,k^\:طGwJjZ7g+re Z/ ¸H)dHn)0F)&T(/V4ϩ,wnNB[Hkzs6l頀y2zOgu⾧u{lD Ό0$ !kNJ~M OI~m,&KE6>} ]Lnd Ҧ Ev {^/pE*X,]T`s<GO8W,VE_lob4(yM> stream x\Ys۶~ׯtBL3Y&i8mLdu#[$iPbvJ/,YP.2Lj^2 ztt5*79g4αp=z&e##QjH&m࠘f2Xcq[IIS3= LY)F>D c`8FKZiiaZ Љi!1.L{GAqI@Oyf3VȌ;fs8``V@{ADf%ڢ)5 cb:0 - 1' AR4&0 #srh9G;oR&(-pT#-' )/(dDPP(B! 8QW/N97x-eei"[&*`֔p Ⱦ`7 FK(;;Nm\ =-{`Pl'"GsQyLU tz|o>;ܯ!){0d֕F^[-'&IE^ti}4ߝ}aIQiAVxQix2/u!ba>>ߗ"2*:&6d'3=LOezYT2=LOgz:әi٧3=Ldz&3Lg2=L4 a1Z摳0ePDx$L(WL $IԔM^+dR%j]*\р)]xO͑13zrZ/F^w ~)7eh((J[k8Ҙ]3ȩé6><".;dUWZq Q(c㎭Q$ & R -o\àP{ڥuwd #iHBnCI_F.Zpu:#}El;$*cEۗʔ@K.2;Je~-_u& 9k[Mk> j1!qbBy8~wNUJhh"jRf -HS +tLĮ(1d /5Q]j^Y~G)RgIu)Ԏp>->TowS Z73`U"hbHEiAKV9K;D UQJQA"ހ5hQ6`-*܎e t-l0*n hT.C˔CMӣ /OiQRv0K߆vD hi?#Mɩc&/ L/?|ҒVs) 0`ua"`h:5!^?(J3aQ^ѧ }Ah5*Ӯ)TZJ9\*TQvDsTRa6w4ʉVDVM)HlAVB!XJRAh#><G*ޤj+IT6mף]0_:YqӖݤ+m m:&u3j(tt<}+uKmj݌\>LU:}})w } 9(QNsSP G:mKt [PՔjA-No(/*!H@N-r^|/лcKޅZeы @Hq2TDOXѶ$(}8-_ wᱫ.PHt.h7(US-l @)6Ja}WV#]krHFc;E?1+He߮bXX.$u+\к~m]om\t"u"dɕ;qnmߚ@6Tnz;hw-Zﻰ^8!WyOۡ r='侹^o!3cʽM4G>*ߋo:Ye-`iٺv mH߰ltzyWQ5\q/٫d2.f X\CɁMْcH^n >'R*ZbD %;QEW+"#V^iT5^3,Uov ,jִ[l6ܵm3 mĘ)#`:CWxջ zu2zq8|\Ͳҳ)r/~#f0,p2Ԇu;CNE 7,飵н[n>Z;gWpaY΢W)%0>gf &x<%by׽}bP"|Jcr%d6kNweבգߞ>q˗ ź0J5J(.hY3s<BuĹg~|69=8_<=uw6=X*Br@1&@G*kUXmM ٕ-ve{>?࿤_?K_1~?aMmz^qsNO׼r8zO_O3>Oł3>9^:9_E}:9Mgg|1/N#pu83MckŦ Bp䚩캩|To1Z g[mjZ|~%;Mj~v~z'd.3J:j\?׋}:-룃is7SɗG˓y]埳-:u`7݄bh9@? ڂK_ \ VJX:LWBFAVek7^qomsKBP];Zik{%ٍ+Vĕ^YyR69ϟpdֻ[[4.Yq&ZzJ^lmvdC2~}n hnT8mT(jA*Y;ٴ endstream endobj 883 0 obj << /Length1 1144 /Length2 3835 /Length3 0 /Length 4584 /Filter /FlateDecode >> stream xuUgXSYHzNH^! )@ Ei  E4((ҕ"EDA4)ܙ8{sDdXO!B 0%ߓDcem$ L̢("M!8އ 05D@,ƣ!`k( 2z юáQ `Ix/{S*wV9,G!}` ['Iii:ؓ6($,FYh,G&1?bG` "y &GDyIJ9)s`0Qϖ$@L 71!"XQ(0##=$?`8p4Gys(x1C`ʋkG-D<* @`ȏ%ʅ"4pK?t1:"kGc)PU%2)Eue_aS7gᔉU| 'P\iXI*Yb(/,I.B!P^j~aDB/g`J9Gh8_p_GǔG0HcBPE/P?Y,T g}?)%! @a`;"pڹ?.bڢ䫮WN`<)in틛ݝ< U*XYM',1 QSg- ў$FxtN\3fǕOE|kX1,6H-kZ@ xvսD+co+"qKJK]*NW\ZJL6v }E!wdjJ׏?ME%y Kb=NVʝc!>NUv0W6}ְ[֠sI_?21s9:Ja!f7lxNY1 m |Brm9;:zYT.<-Qض'{6J(Qh7Ka/󨦁AfR5M|dNhV xėɐ﹋G?yRI/)KjZYi]p1 2 .MtHwxτУD͖|竲dltJ!yqβM^!$.)r]cgvM F>;~h{9:|KZ2maۂ`EQ.q}SM7j|MQ3RA*¥WB26ک 8?^$zl, F5ZOu5Or.?!*_SF#Aȉ5}kF#)Lt,u+|zߛ{+ fFڤDt8?4ۣd}FhсCݟqk36)L{|'G(sNW{eʴyRo1^`F2eZI=%չcwtV,BxN) 14pQY+.L Au̢O{kY6Q MݖOhEA2VDҚb5 M2XV_lj.46DcYmExz lr}s gz"ὡIo39"E.`R=#?8bY#8|(]sݴ/YoLp# ]B:%x;,YjeH/BdutSRfob\?y@E&pm=|[2n~;W)y`mK.e1:U I}ӻYEr=`g5?Y&ӟ.Vg2FC>LG42B٦{7S4H8D㛺0;m;,ݭy2F{d>(pKy?1pft^ NV[/.mʵL#?r>, ؉-~?J"]+OS,1gK 6:%Q@{wʳub4tuurQFL_چ~E\4{Cc n6xuD|gc+U0؁8Ś9Q'>EU|AyuY:[ޚfgKm+%@7!i~EmZ^[g[\L5OlڬV90F Ր:pKR 5 @E(g7n©B;'[,ztURUp}kX,rוb]N1\݋-*l #x=>t&uB%KoSrĊSM/ |{T l2+~hF$Qo~ o[׾Ev-£BBa _{U qBξTru`C_%@&Ĺ}Rc(5fg&K-P~?%jo]0!lkHx>K@y|УO\W>}h, ={X~xJ-oosceH2z6oga%w:މh[RA]3uƭrJ1w]،o%aU8LM^oܕM]]g/ R^bSҖ(X}HiJvM\UCׂG$vxӉZ;)± #{iVmʦڂyb:3_$Go Er埱4's 083 h)]²ZK7%)=8<@Pɛ of{@Iݱ*+&җ뵍 }ᨵmGdP'~SuE_Jn{ $rK5z%gb Fz/@s>Ɉ{i)X \EL.p 9 W,٤e=7yvxrl[wsK )9B]_`yb3WdxMkZpcދQU"Ldn铁j{ 9qamM){j;7О=8ꐕ*a>'k!m!a&Cs7>]n99_jޞ1KJa kojak;w4Hy'F[D^n Ogf|]7HO,O6 #EEiWZ s]hua[N#\T.4 Y"ge7|L;m暸h徶VA3*g%l24<]2"Ki#1g4?:7>rs&&Omo6u_۝\c//__˻Gܜҫ9T͢oK^bdw~>byL (ܪqC|R5HlwPBDu8}RUBj endstream endobj 886 0 obj << /Length1 1626 /Length2 13436 /Length3 0 /Length 14264 /Filter /FlateDecode >> stream xڭxStnlYmNTlgfŶmb;8bWlc>7}~|;/{#SI{;  #3/@YVўGA瀣s25ZۉMy&qSc++ fdinPjS ?-ΖvWS{[S;guSS`fic SR֖QPK)LL m.F6yKcS;gS֜?s8 ƖaƦNΟKgs@{_>frp}&Sw:;Y:U%ha`oiboWK>|Zv;ZFKgCڟ,ligO'SsC'Sg4? l<0XMmXX?k?k[1+2vfM\jO&v6S38&E{gIe-[#_9o=kjIECǍ|C;:4.W+_5MV_m2@ϑؙ%`fh9v&N6v=R 3,,"&S;I~QPߎʟKTp(؛WQQ{w '7}dJ埲!73޿37km L>7]> v;ojnj doleq(mT+*K 4x al}oX:y^Z WTx\#=:Y)*496d"Xkq2/=tl8"PSߡ~K ;dquKm{.>A+l^,Q|D.P5.tWNG:&IuLRn Lɼ7rU`{A/\jPYN kַ> 당GE}ĹV0ZM,VͦYC@,b9~i$@:xcЬK¾wb>oSWa޽)A C\ u% ƞ R_t0Oƒl2h<צ3I@m`=Jtg 5x|Ե,t?1U? ڴ/ji~ͣr@y {A9B+;ҹW+J!!yl@ʎ]xZGQW;R>ŗх󵎭Ð %Ҹ9̟`;eIyIho?Nŵ WWoسz.KcbK#be-0ZTJglj lީ$%Юրh!HHF`*/8ijna1 s=< Ik 'e_Կp<(- +ɩ|[j&FyJB*XZ,_)%Xn+o剘kđA}CrsS#I ]×hTTd33U>.Y V.ί,MK+ǘr2&*SF$͐.S {E*>Oz*+IS8Rfmowt _5h E$:~6 Po\v]DCAOHpZٲm JirYNZG0(&$)9R/u/9x]:D :'߇Ip߻F,l0g^{kϟ_ p(^uH/5=V]_4L[q% D065aU0V?uFy"s-!mPt>KP|5ծW s_YҐ($۳r͝a?: ^Sݐ|4a$0HOEeq?u\e FN _o'spV4?{ k mWIﮱ~S^MBNF-}Sz'hX*&PMЧEcLUv*bCSHUJ x0}]љDO}7)~i .\3 Da]J+=,.z% Uu.WW//.vDTep*LN?|E90Un@MA OduCBBECΓاS1QGe8QBTĠXH2o\9l&B EI¬-Y3^=,#,p<xy_n*w}NhB3keɅ{7Nd$:8kNHwI7k:߰,.S$Zn1tF޷_֚gZG&!f=Mmi^fߺn'K59-<}#:o6 bV_gqѦr,lJI7fvίTJgaZAG|}B%Dl/}'_YO'L>ox>n_g#o:M[P]+Rtz>x^oI{ 2~p"Y g,ۚ<ŝ1E1 &#zYYffV՝L!bAXyϢpղVKyk;tkw\~=oc)j!{z?ivoRAwQ]a!͈c̠_2[3wpo'=U=ҪD{T*VٹO}OKBz6Nȇ5wky>0\"zqU$531y0io7Ð)gr{51]=(D{}W7+mG^G0νu+[5D`EZ~} _B'vEا6'vЂEދ@p!MMxi.rzl`'JZ`sQhD݌bu{a|c21&zDF>Α6y5l!lD.ov"]M}IF ֙SIwYX15 L[sR{8 rp gw4_A:_V$Zna/tB6$aUzX1X>R[qھ;p3._gmIenF'8Fc0"14)'W`њe*:v)nb+74g𬍳 !q h '*^Kh 3=7th=9D˟Eg9q^qAtNKkVd،((!,_ũ Z|O;g"?,$-r@I14ABE)߶?|JUD)SxTާ2TݗC_n;b|z|zm؁/W DžZ7'Ny&ZمSW~Xq}Fma|up?Nb•vC@Ml{OXB犛->tӄ(VcEKX$(RA6qQV9w@ﰗ6%4;_2/ Jv,6X9_Ak4k3!=Rh ZZjt7M pNO704E6IK񧺪}fIpړzkpPIĹΦ7~-ߡ*dtz[AJ4<,d jSKOߘ5S-iwZZIYX7'^,6Fe%ΛGv/yb{"Rd,rId†BaaܙAɅɀ m*r$;,W #T]\͋l;dqͱyZzA4s/Dx]\]I4Eu7) E)enb٢A4VD`!fLu@+}pZR%$ (0&|Dk9%A1n+jNn(ХeSw޽gZa*_hY"* r}0DM^!XN9w]׽hW12:jKw5Rpo?6*61$ 읨V~jwO$Ym~vvkqFWUM55=Iyg`&t{*N_fv/nw#,_K* DV-˩O$zh|<wG'we}]k(N5s8<B"Y~ CxPY9t^^ͥ DAd-q|p?zA3rDkx6jC-C2FqI@5jh6yV  TU9~bCJm"zo憵ARQjP^Mm䉅N}hUR?[GV(ȭFB>a3&p g/vjn`~ ,HU)."~.Ǫ ^#y- ),󤦜*+S.rNTy>eCsst6S YHU9z$ h˔IboudM%*93f:fsk3>-zwV꭫ JMLs,UY?YС4yڂ1L:Z1O Gсm:Wr}bpöv(okK3쥶[ç=YIL`/aebzi{_>G:y҅x<_-/,d5],i 4JF\ IܼE,ɨdbi (!]pɽ Ώg]W4ܥƎIIUSrƥ9= ;.d~pkTr|_={yy[.v#Dğbbc~;Q}/r~УuP#-Wd?Y=W.lZGc0Lp )њ9gu\<[e_$ 42 bs7?m ~  `/p{6K6 h/fP(46`2.`"ױB!R̲i<='{!'W:QI"L.CZp˹ڲ&^yh~eMRƆEw& hT:)BCa/E-ȼVud~2Dy5j#P.L #A.vuO`;qm䬢R=%nI ,%%sKHXd ,h1J F{2lo r%(k$Hp-Szy6М4"yD e'sOH/*,FOvMTKkL:gJtdM#40G9T8/_k6vo2AGߢc'ݬʞ}}UplQ# jB۞`ACF eNZ~KMVJZYx0INnʋJqX_bmә@4,ii5#Tx#8GЪ̾&>R**<xj~mdMguZU7kgWlj6c B@նee a(=UP cc߽Iɻcq:U;^Ei'~Pw#E&{Xi[szAl`^"ʲj`$IJA2n ߰C;HEcIB[V急݊$)uq^B~ B)_@A}} *TQ cfBQXbp2*u`LY_!GfSnJ0ax7SN+<8_Izę@szt7T pݗE/lh׎~DԉtM$@+Fy! TA H{j ܽ^$JQy&Zu/pƊ]ip+U7xoC{A*,a7~bޅ[3V4*i+# l +k^S 5A22B p3ݩAV7҆ˑ[cť]6lHpxExY Ť9u{}QO* wlQV=lhd%Rdc4a<)`!9He[F{-}qǟS_C^kr>pNYOW{y&4 TuD %cTBlƸ69wq^d'ΊMH9Dbe*f`|ZE*^6f`:3ƛD#\f;F SM➖L @_'mXC)2[@-u r X] ix%mD4}{y}*GÃ|8ZÞJt@h'9֥\qݒ[ĕ|IQݸ2(8p'!eUzD%<ocD߷{jM r]ɽ'L"f{1Le|dt26=6TR7tz?R+ȳXZY$he~z>[x '悑e.qT0F1h+Y!&,c!f DISQ4yK;nƒ?) wl^ߕI7_e C OE$ {,e"T|&CQN8KHjo( lӃ&8o~0 CG[kPF{AQ6˲!H+F=-'TJ%9a0O[P2RٴV.-䄰Ԡ= r8DīO$& Pf=:H Q[?R F2==˔qt)[㴕&$l-HPADe23#AJA7,;*K֊T +fΆaq5/E]!)xtUS N{I t:2Hz}=r~CNc̳}p=sePS50b(c?x+;!T 3IRoeytFrt'Zh洎䰂4ERhքDY1 ހu Q=j3ߊ,t)e(:6~!>Xu}qցr rʐIωz!l&fiE G]Fz|lXʐd]DrekQT'?`q"%ޤSb/t8(KJ}?IBt[5PBwf\-WP18m=6UrOc#Da!%W#}}Hp͆qw6/Jy 㷚 HOי@Hm#|iDéAWP]DxIũ^J$H }3 4g> stream xڬctf]&;Iݩb۶m۶m۶QI*m۬8ӧt{&k7)3 -='@IF֎CFWGJ*hblag+ll P31 p!;{G 3sgJ1z毧-_kG%  $'!!+ Uؚ8X] -F&N&S;G#;[cJs%08ٛYu3q72GE 7qprp9:큳M_ `vNNF΀QE?,v-\)_0Ngwb- < fh4\,l3j_t?/[{_V3 g'kSZ8ƿ16gV$lM _ "gf(&a`lgk061s@2 B -bm-k`wc-HY4? kV3w0 gm5K =-N&FS=\/j+-l!*[Z_?4fP 8+{T#cg? ڹhX4 ߄8}7!ggG wߺUx_`Dl%g[?\8kMLM֖팸-ӳ2'{C˚ kzw9Bh9?=?%)F{1\SluQ!f\E{,J@hҫM*(ꖾCLw09?wGKerNd TݪV\vNƐqDh%z@Ͽl'v XK}\hFYRsYbu3*k#9jY,eE_|p1K34X\9CAC5gT)2y$FهF9}!Bf&-_2" M9o,uSrʲz~x /% M ' E|˦;gD}nkVWwK ITGӰxH,ol/sҢaPs3Vԩn^oԬHd., 3>+JA8b{BP=d:l#C3 [˴ R,puO}jZ\嶙nEFjꚵWA|E1Wmߔa9 o^lh5jTK <ۥ"Wl$0ag"M%MS#fL\>'X"#\blZऊdX($",^Xd$"^p?q/D[SM~ͫ5-&^]܆o%,B2b_-o $蘬Nt=^ۑq~Ejl;Bfo6;-f%ݙ1ɦ6ªͶDЖSTU@so1j5{l@w8Q8Zv2_GvZZL21R]o ؞Ֆ;>v,SUI5* l\?.Z@)z066}3",gqpWfYX} 3T Ey(2d<pzp2`lĔ [ν^l ̀ҵI)BFs+~g⁄s18JuV/z( 2U}꽀ȅ|D)Oy4bv4!oZ$"|z±Ϊxk0/Dk+QWyDl'GBuKW!lRk)>9ڀ{/hDħ}syD@.TP`s(1$ o4><P8gnڴ%ZϺPS| ➌iH 7݄yoױ; $~>_j7X|mQjqqTp(AĚޚD\$DLX'gdIl?C0 z)D"+@yq̺%6y$b@J ŋyf-Pdu~LL6ifh 36;/ @ 9=J>:RF8rPL>/[:j6eFGKMy,42M Q>k|k*W|.vEwdK| \:E\L\n4Mw44¦W͵nF)0<[FWAA/4IqĭzF;yc>l;x-xUUϜZs|`ztraރG/2@V"L)7 >Qn 0`dHE>r :%d4G5ݱZyRwhGmF>:ܘ=۽Yo~Z;j9=SwbӼ֊͕%ԟ!O&fÅ!h~VU^&em K}25IJ!R -?[6\o]~Wi|V8LBxvmrY'?yq,P$轁m%z:"!ƪ@.Ԧ2iIǔ X eCVdɐwYC  gu[I nݶK"BQ Sh6K i.M43Lb!c]_1V}[(`$YG`pKC`!\ebM)3%h.b]8Q''qJ9Ȩ3/ĴY9[7v ʈa lW9kd>P!lmpө:,v%D~>J NlwóJA s6=˜cj+[W-/i'YI=bx)K{?q=8C԰{ybSݤ)Qb ]mdW3lAeX" "O`qړ]Oҭ$ALv-Ϸ۵S ^f,w2ӽש?쬐2KL>O7n(C7/ŗnʥ(uPEinnO)1Q6^/Ah}D/$)6sEWD9 meӺϦ)^Rd珴ϺwOϩы-|ɢ813аj=[L8G-ȿ Y1]y5 E FRS>RN(3|/s< nrQ^Bqn4Z((6ĘHX{/h/kNZ󄼡XKFγMXrDl3U &@PHoDuѭHŋXǗ mCo'1P{b%Çͣe)m6M=@aEb&כ<+Tz3W] 3㑴usIi:IAmJ@l'vFį,L1g/SUad:o ˃M:pQ6hXru;W'"V[}eU-[ (X51YuN_piAO2vik\/kɬŠGAPDTK׼2Y͕c 0)J K/E8M/qqJW t˭KOMe*F4XZ<<"۷z1C[֐( ^wfx{f{K^5ýdUp2y3L %bGQiN(߫#W4dSi=;#9/&i *| ?O#&~KEIQH Zɤjp#g~k@ⲄxLqE3+>.l 3|mIHIw \sCrL]]MANٗLvej3itL6XJ-?ie&"t޽Gڨa_b|Ǡ#i=ܭ߾h  Ya`„ Z]Wzu62DZ0-uRd-S^YZA~'>CkKථR_n8 t)S0,XJ?62(qNW' d*<+-{ BhH{/bӷ&8^:o~P`>"P[0WذOW(iqz[Gf@.zP wM5DQ~\(oZOykro#%A1?# F_YF t=ό6-̨U@DoL(k+u pOѻSp+s))5ҝ1*#!}>*MٞlAeYӾ )7pMHѢXh@ɷϯ{'zw xC Ibw-Śifx ǙV!,7Iӗc+ s@w+^zs>MEf#'f5y _ sARO=:YrNzR\zg_6^d}5cǓQSD8EWGB1y.9v$ %a$Y‚Y!a[<3k~nS `>x-f娙S TRi8U}4~J*oCfXeNYXBzdvQM`Sy`yRţn&(̔/, q HS6s@22>  j/H>G;r@ ӆG; UXȀ]9:_غ@EauOQ}Qӗwɴe'"y$a 2&򾢈}0)Ub4pr-2{n'mKcK`\zY2ɀt[bI}PҜ[]IX/8&N*l߾P?Vڤ4،FIke;ZpjE 0N87K'5>uMU.aߔ#3T/$eb'HT A~X\̆e:*P15tgCKu"0Ib72?_PS`Ͼ۾6RECuY,۰r+3rTu]'a"_"X7 R/Ka&Z?d'\ygY\0iSTX~Q`/ouN]>B+~ _L!en`Kr:WJMk4o0n{5G{_K"tc#]ىAf_w{ts;5k-N @*Uj5V/-}<$xXRӽ[SZV^{{]+.]r!{m*''TpN"pȗD@r㉨4}i`hwUѪxctƅ Men׍X~UFzmQ%hM?Fw>-F[3z,H+w1L-؛i¾tr*fo_$zSĢq_2v*iN@iO׌L+ZqL1">ՖQU5*_dScr8O)6%A$w|C_AYyڝۮC$`_C9RC5t$Q%qfs ?{x*`-̫\\4l#u5_.,,_cp*6d2;.2P@o4EwϊIQP21,ȉ{$^Jc?fe Ƙ;R9'`yoj(v +m/RUtlt4$.98"LȓnD72GB8ƨxŻ|p[c-Yyǥ?kۄ/I4`3RrR܊uL{iZ&ZNpZCYAu`Q9T _A.?:vXO, 4GW 1l`qU?EJ&@~'Ճӹ>nfxZ)kh;p& 9CSàUPa4p@ WلuɟBzTd &ǰ5#Z2׬ Xi.DY^#V%ʏQe5-2%l A˯OW=|FhW04%u>5)r&!kԑn'Ub\fszB,LpoAӊ#Bh-nh$SE)\ND -fl{B%ŭ&fD%;x2זY9F'y?CZqMDD. "D!ߝiIvDZ= &ⷅ@g,\ɄXԚ;MIv6d`s\[j42qtft ҋHma#۹cd+qM`K N7E' tm6t|!4IL^tV*fTӂ^,9&D&̽b )c&,aaLjʋg'(ϲs9*g2֓H1[;=V||^U4 \^t[e1t … YwMIlZbc>[?>wLcfFWj5 !2g|tڳlsE;E,'ai?oW!Z>tưg?g;VށCmǦk$`H 6#,kdF.t\+rTӮþC6spNW!5""ь<@̿'j%ch<`rq"0~JnD.H'ӫ&Ud!YZ٪jᵷe-5Cv4`'túӿMyZ{qلgV&GQzJ. P ^PY x%p*9vHR+靁<}܊9%@TTB <,+pmzIl,T*-L;fti]I}w~"# yIoJd[aթ;S{ـ ZbFw6*hFf c5Z-4;7K)><=s Y#CDʴq5a댮N 7tc2 :S9 x0o&$`eٜ1ptʣF5塜c|pAcнܥ>lcaPrD6:yn !Dxe*P ox۾i|KᝎM)Y}BЁoJ\5]Yy ]GD`% u2HI(wX9ఐT=D q`Bz%Iiׅ>`)o @lc3:V)wP P>7"{SB*$ SPc4~ɑ!z监y;^@OrH)tPbd@k:>nuA`閸%Cģ5Mnx=`bHMbdGT'tB{1f fװ LPo$5JCt4PZit؝FW|$wIhVI_ a ~-5ݼs h {[)$[]$w@6h:f˵;Uٵ?Y]je:AhH%b V H:gk߱;>`L7=?G&L:oa6v*0 ;v`k`DkjR^K]O$ q(Dl0>Ic 7(O7eFKI׬߹БD+ydRUWlsN.7G2"<"!U!_zktf2Ga6HD%gS6dO)GFWFM,&C:*\qsV8mlm0~i8V¨=pqJsIyjo_MD9 h ]e_oWGͶΛ|g-jm\tAj%ͥ7ҭdsWZ NzS_5gLWn*Vt=LJ@ yc.o>Gݽ(1vJn$ⷲ gz[RH1ӥGkZ\l-&kVSB,n7P3Y*-[flG,P~43fwXt[P=oHvD6i# Eqs/`^%J=OB)&;&WE=}Q/?BRBƁ!wd7}sQO4ѳ#w˵ $+J-D?9vCd/bd ꧖j8m?-~#}Qn3gy9,3Yo n΍M&0F^ ~yBVT hV_J#YK5&6[H;HOYjWMrԱ9@5Ql0;9 Z=Wi@2$ M}mK'ynZw1YtB갣HMټ{u6?*Jj)GA3Xoжi训UKK,0D-OمsFZu$!b4/Mk*4 VS~|r}w4AbgQ킠Io,{[ԃd/>N%jg­þB[zSTrQl}\h3RlD~#\?WYCb/)b邀%4,DyRܓ*zY$%Xⱌ"6maWBb6z:>-;Db&̷̼ԳVweBbEl9{Ej[dSi%9JP,ӫI"-;IgL4TV\S&)E)cݛ4%D{qՔ:]Ǚ˘C;"~AIF i /{tem?}w8G421S+%九Mc˥r.qB*P7_R,ݹ2mۈkON(3?.Մ 23D"r"E_-d8nׄOC(ɰi%#L'fJ.Zi C{ ETrP`ag1PgCaՠV+LWnI$[C舾j @\r1ҕjWW2dD PޝZ+ 4~.o|pTw>s/g[ff~cdԙCH%CUP@hRn2V.L0QQBS#"֎T@jME,PEUTߑn>tnIKm9;"*Qa>='~"ѸT$!ȗQ&rkwYR _D?U)nuNk)5L1p?-Q;cjF(F|T/OeF9h7qoYgV|&]qԦywCWД 3v!SJB #E3g+$(9@,,>@pJ)Bu:eKW ꜏n[NiS%!/}̓YXi<8lv(z|<eBAYgeu!)*>$iq@NlR=D RĶ2E=EX+>3a kOܞRl`k!◚Qg#2%{8k!}LV^9Y@.1X NeK vYA83XVW{Ԥ-KRW_G&^dJtGc'CN P[RF'D{Lk2~VѠrˏ$q슏BVȏd#&|[2_ȞdBr˟bn%2y5h{J̣EY( qSKj^w (#£U{. Z\ų,|lEnBQE=j}J=}uEx`2M]6h(t'zwz)';pRӑja r Oxo?#u} ,1KбtGW Jx!e'ЭJ)`ؒG/6i)fF(f-&|Mxi}hdowe?0*VEjHoكwxֱ< e-LtsD)I WhjK*j(y'4sbE}}нbZ"h$#ԡq"X( d=Alw2xS60S(A&jk Ո`j)5JP>eҽQ]:oY.^K*q34>ȿDvF&DSroCla 9@=Đpg yгQ ? za@E/8;c\رב)0 QO)|_Hk"t: dMy@~RS86n|srar ueyP[m&=\Edp~ڧ6>H<[ZAtম*hxb_(W9jE&O^1Ok:#vKxFHλJv(Mƈ_ٓ+0 ^_h̽Q 1㻊8ڮ*zHUSVbJGuט)d8B4+ɜEL-NdVTGKq|6r Ri}t<~ nAADqk]Cj|NW#jú'AW+mskU Zݖ2g/0kraZg}u(Hpjٽ^-ϮR-])z`{<=ZD#vS!s!D8\ =@B#yO,;LCoN`J445Bn(^Y Vbz ytF%kF5U"xcl3DGYv pn s0aE@y6s7h$|GOsD})nD˟ Y[HY&n5E Y kʳԟqȏoU%쑖Uocڨ MxezO]BJ(2L}.Ncswf|79Ye)u0R vĿH~@C)IN/8͝ŋt@CE%K߼8͝%ᖣܪVoC5&e>cYhf"w%{br=U|l yYl`,M@qOe[ J,Qi尭T:&1Z0)J73'ŵ)dhu/i_"hӕ e%'+? Vt>jV:h+F",s۩b'wKq57$I[Vk*'\QZ~dft>ըV1)H˝sc-lF0WۅIB+ԃG7*Oy潁>tfO1+8!"5s$|H0Ѧc k #RK(gUVIɓfev&iqf&ON&+98~[ )UY_Qzp4]+e:#q-o׀Y9Ӥ/Fqr6A Ұ5N^k-S)H`? 4]h#oGP@NL~&^RC,@i ͱ嶎.mVHhQ蛏 0;y`fSnV_R<W-$: Ww2-ŕbʋV56Mӓ"^ DY<Cmg3DE_V$i$F秓^Uy ]7Q pz.UYY+5Vq;jTD/\F ~|_{PO抚-UT/[gͮSfI*UqDM_ʗrZIXthDlFIo-`1Wod]:sLCCbԄ6ͶU>Oa汹}l֫@9rU.n°[蚘KR`vsS3X4zY/b_!&g||̗fFn31 o)iv!;@\JYza=(Oi Qfx.Y&sDqg<E/^ nuּJۂ߮SÊZ-X_!`^4%v$}k5‰)E@pi{Q~]2қ7+c\Do]N2f|g :q~ cNNCn]?/e+m![#u]bL Da۴뾘k!j{?F լ~2d|A4~ȌG(>18{ +X`,RC^ eK(RwC4j`IBc+LUf{@c_{/r]i~(.]=_k3c O~=q~DSU2@oEF%e jt [-점zAnBɂ`G\v( K@T^jT;:T3&?9$}Îya~g6Ntb P5&R;ҥ0h!]ѷA%;n:מJ-S-k?cBu{n$0̰5HC Cr7ꟼ9fcC5ZLu|6`ic3]:XMi9x3 09!xՔlV^\T:&&qkBbz찄a|CSmIeT1~喓 Ke3Kr&<, G]X^]jpz*Ɩc;';8UR !6Id-(Luus6qbknCQ~X>.UK6"ni*f>"3Q-f?B %U=K2W!r˝qU[IiDjQP I,{X{Ob`oeD۽3}柾Խp w#@6}ONFFׁjoX0RN-Mֵ qax-?XyIaE' 7;_9Z!a era˼@Is8 WHIJh9W!MU6?tu bH1Tjʴgp%BuU%.wZw 0/9._Ud=sV^Kz4ۃ}``U^*-M@GqJ.?as+v ߞqۋVSܾ-;a1"}z҂ .9sۓ:i驭TSje/ɹ;YC60WGAԒs H+/bo*u( 5U.1U"7.\PT OI.,ׄ2;,*;nLrItێքʬh> stream xڭvcxݲmccŶ1;ݱm۶m|>{ϟsϏw֘5j B&vF@q;[gzf& EÑ8 -lE <u @h `a0sssÑD=-̝TJԴt`O@ :R@9`ja +hJI$T@[g .F c`j`lgkbWiN \BNC= n m,>N3GC[3pX[>w|bd vNNƎ΀Ϩ N0s_%}|ΆNg_ '{kC؟ddak @3CGk''_:zC{{k9X8;MY>c;6c_lMLs:}@T g&v)gHLe=$_E?q]rSX[|6? s>g @װ6t5p,/WC kwp;Bf 33l$n4Qp66ZvU[-SӉ0s c+ۿ`5>uFuU1E)QfY+U<쁀.kg򟋿^zNyYؼa&bZ _+53 mM>[? .= >+Ky,R2D{ kUr*:}CK ^+&xޛ=uaZSv&ϳIsP)Z9i SN#.d6!8 ^&ZYa.H]s|j1ڐ@PrO(); hGÒb$;{8C>r!zے$ۉT{Jhh3z}Re*rDn:D6ڱpLs|IP?P(؆ExUMʔ2`4baN.ΨȔ&2SD1jl#B݈ 5N Wic$%wħj>wEQ(yRG;=Oύe"!v&^H葖 CnJpŸ6}]0޷ ~Wnܸ`sT#%}K?cɁ95䲋\?]Y#eQFg۔Utcʒtq0DζZ3+yvϢ(f>mO`ɷnU]w0lᇮ(G/%]?Zw &轌QѶ[iG90+(9 N&>VWŠ"aHw~C/^b\UOhUA*76h)S(A9i-Ƙ46eg:f[5A7SXKlB*|8Cj()(u!XN^~Dx+w%o\P\U7`ZA |r$Ht;\=j NvGݰW?FsbGZK* 9 ,?)cu\û.O6 vn鵘zI0cK'LK Ax<?jq<Nϓۗ.|}g.0-s(aʖ{׊an8⚉BM Vl#^ MOiZ2?;GȅF]|wI*P0}#`;`;/K \=C}ͤ^0lm!컼~ ԍ*\WgRҏt UD|C|<4RpѸP3V.XHµXS!6+p0rl@N>0ͤ3} k%DX٦K#1h״9|ȫQʹ|"q (4pZ//v0a4ԁv[]r˲ư.36]7g׾ }),,$62(|ðٓEKd$/:*rJ?(V |uuct|Y>饕a[w0,aw~DlX0M5^AOAE|5WH'Bc!h㗅KL.vdzGp-"ػyS(KjhfV9HCȠ9AasT%ᙻ-q 3T TTe+|xza7Da#<4.}2ֻv-;{~u- N]$#N•S[S.hk >Ut IFR؝pXh W*"ǂXGTck2E*73`d±Rә)UͯaQP΅)?dԖkȍ +ViX3,P RcX߸c\z<1+ $%jO֛sey%]% %\F 3!’0WY:xWKLPvg 'UULDXcJ@rj% 3x N @0o=!NXخՆ4]æjO/;DmzLD8 hU?6c`faր.$2vu愜`a w,*\B.B+;+5a&̦3 [Xf8ˮƈkR ^}OP+Dz+ڷGј Βb _]IPL!nTzp,mO{29X\w*bTkT'&`l"\WCBnm:F9M%LKo0(e+bP{C>ڂ[&tNo^Q{ c_:x \9,mF mXւd`N:cę : h `"6H 4]F|QNx+P(isN~@Ii@wc7 U-)ڋ~lz͞ӴkV g䯘a8bH}nN>'ѪdN ^ZP|*l>lH^0Is7ENb<,h%܂C*J.uZTTڝVD K Iic-r=זUXm=;ʗ8A=N[M"w5í<@BK[*EBXW!.FZ LQ/{D]?ҀooRuyaqVWFS)vtK gIID+}vw]= {oL/g3 f۶yD]"s+h %L}J|a35L_h;ao>~|a_VWfGHgU;Yi3uV3># +z:aFeۍ^QobTv@v1Ժw(짜vVA Z32My-SE%TQ[fqpҙ|:BYz=,!9lDK $qB%DFTqq`k.o*mz_7z4vrNT"Ƌb 6N;D~mt|Ɲzt)4-ZL4DRՄco;%xԼTZ7~+ V$E Uxm946mW2YwX KB #Jdܢ/X$! xsPB^vHQ DA&4k9j9i.8t*A£Ɔ;c=αr% alJZٻBZJu !Jz7:=/ 4xfё+۵nro 0*inICdwՙݨŞ\5ʙΊn]wIo$,c,MBЫ؊;?!| w Bu ,KDnXtjx 5CQI@?8Y0Z@e.+6^d-U9?!Bk;xEDl)JUvBX ηϩ=֮!r7eYG'F zF0_~XÜRߌ"# kb0ǯG0#j BQt@2.'0q $iǻ!f$xvYȐa#U;( #Kk'dAмe)򗯜\~k4˒?R%s䭤@HKUӆ ExU![@^w ( 9 eڠ7 1G:)T7G/ h1ddˋH2l}U>3ns7)gzI÷ h{F t:;y&,gF<Ȟo]< t :O*X\$_xN\vU;y\=nO{oL]3:eO ;E[p64* Xmz7PHE%ɗ,$PS*9<p'~sAB[.la4\8k:6x}qF5BUFUȬ1 / Jp;Xo% I}?xCy1՚HŮ Y!|!izs]"ur7xG@L8:S.`< ]['XL3jLϽ5(fZB`)=OG3}0EgO^&R%$#X%WNۀZ>}>fu#͖ ƕK$"ѝXb ;jî&䯶YXyk_o^j/INL],(+JS!|=- rCLt;j7'Ժ=}6zStD_Q&n*OR$ěyi=qܘ^wy띕؇sV=[hH,:+LC[tШ 揩YU5r&>* nl*)XOjMWEbghFmB2m^72e }B1tLb!b GЍׅT$b/G8' gl r2<7=.AK4h-fX}:OL?Iӏu/݇TlR} 3(|.SZ~Ri%~ejsc]Խ"F2UuB\FUNջ$*I9y/zV k/c oKТ-&Њ3c;ӱқ[wk')bnxx",rsVgEV Q['@vtzT^[؁qוŘEKMkLFGef'7HQY_Gs56$%$lP Z W:R8.p>~#ҹOCU&s)řj{̡)s ..wm_]Z]PՅڐiЫHvMSIϖ5FW@F-d5n9~#jyMuH:%;<7V q"< uY֥PuZe`4;@f/k ЄPh~cY^{XLbWh &F~Ʋ3Tx0Wi)P(/b#?nͷ,(Z%;$r/*wX _ ` 7GOB}L=xEaqp,XԆ2W} )_`SG@2KaE.B/{ A!fFwԽNX)ͅxEbIrE`Bʦ+ #_ຓUUߏD2'(J Kol[_s= 馼ZMI<&)t3Q%cGqOS;R~v5s )˝?x ؗ<=mgG|j0g<~=?Im~zr>2aXbF|oC4 >>'6&m{znUQlT *.w҅84i9덢 UhirF1b6?g;*e̙:П!ټbv[*pQnȂbz9sɗ[K6P?{gw6;{7K))2Dx*|y  .24glu*wםv)aBP(mt+>9[Stu]9HOĕr9=e$]bw뫊mXQ6{({do %nmբ{p-驳Gsڑoo')ĉyj\bCgKL6^-|ly]s$8-'`I2XZ]bgeB+7A6j89Yt'eo؏K^: Yd&օƍ]%|N.H;ya4Jf`G*=| s8x0"i}3\g&ưJ&{X*҂~ېe%v2?7Ŵ\8,yxtVˡև-s&͊9Ug`LF;%(gˈBh)"4M+}kwNSV~2(KL,[*VOl(5XFQ+гWRw$8 i;!rL )8ėQTp-^2uƈw{M $!^ 'r ]?3 %":El8LwhO*^?Bg0N1z(Qr(+&CDi,|] -+ @Io@jV]:Zxcm$B"噚mִEE S4l~.ޤfg[=Ơu:떑I_#-E?sQ}*.rqZGByZgw3i/k3\!f`]\qO/z0 5?gr^rAv(ND?mFVP2S֒GtqGEl X&Gˁ9#eRrVl=N1t,Qd&(ʷ (F6G& v^Ez{XoI2xvJ>>IHwjHA~Џz}.t[}k*Uq!qwywUA!1l~03ç!Yy-}[} y9G`@~ZW;'[##7/rj8G`ؾ"oUN`ߴhnEĜÚ&tdb*-FnCx[wJRa:QՋ&.'lV ± ܇Мoy"li㭉aX*3Nˬ2M9GY%V8^C gDjfc=J"S%]R]QL_)NKqu ' M[H+ܔPz`:^%w_D}9\E.e,CBjՌ'TYSEoMadӌVsGT౮2=ftO^P5!K8PL #'8:d7PNzrHyKrQ0xڹ!A7=J0:sZIufs Բ:0K*/E5JS!+c_N,IiEki:on3-_ߙ!n TC+ܵgqTlvFWG)'n :0,A }KziqtbB*;&d x' ȹ5cj eO^Ӥko:"pr(J- VYߕP^]9Mݿ4(qJor=%MYag9WCs2kV pӉo8NQ=[Bb߈| ޝ̷׷>m3Z:rśZ{2b~4U' "t8lg!݅;a$j`Cvz@0II3Koh[6R}p.laUvY=h..0z (:5 NZa5[Ke7Ck_<*4*x+=DƒYFmmіe=c{ٚ݇g73K.j4kUy|Gw!ޖ/59RR\uա'".#[]"gdDsHSihA9e0b A ,Y!nl| !xA s۩&yJ?K=# HZ4(RXE@һFNA n6R߫Ւދ iG"cՈsv$ho>vBa# Ŭ[͆&*ie| ?p#5V l\TӨ!}Y_o_tgr G:{f!VœyP=Tg}+$'u_/q:W+0GRfX(tt!*!Zp+Q+;9< A|l-ZFrD'>Z*`dZ{e7Dk7:W\;$Q3ZHFְZBeppl ;lgYZkڤC^jVr{0M`$V4YH}oXAn fTѶ>LC..\?^ 9~`諸BýZz=yxйl4"ҁ,c/&ic`9p|nV k˩}UwgwZm䩇9!Z0=_s=NhT7jP7MoCc ?L<RY]a/3͙HbGj|H)p#'9iyub |ҹl|QZe)A{:g 2/ ]*1ў’ƅX{y RDcM4nSj6g8O4_O=up6`AXK8PvVBjTx;1o+ꛢ_,11kr>\ϻfuqV(5#ӥ]#J5%bs'bˏ&7bsĘVޥJ[GM̽@oe1>@(?+J48:2BUeA$')$!@R<ۑ]QD' y6M),M(~CScZ)KDH;dCR~.J1?Lwr|P=uax2M9処xZU,7͍|ܒn˯mgi12B -(g/$P/ak11x/%X}$ڴ?dm)#frHQH_Tb, Q(PJx$;U[Bؔ>Z2]B KQru>ʙ~STj㳾QOhŢ _h(c>2}S_(cd'f;obT<gaBEtiRr?\,7CEEC`X(btqȏ2B}Ԟf)Аojk _! K/2wsAk2LVPV]l'3lAL4S gKkO]{%sNnfs%EyQR=|xyyc@Y 2hX\lޱ.%0Lf‘N|v*1iǗk/C\%FQK?s"D,@Z)4{7SlRWh MMdMnVR} Op*l0e`np> stream xڭZ]s۶}ׯcs;.o`әN'N⤓flʒ#Ji_w P(}=1Ap{p RVXkW hu0fJ "r. g] ,/֎ׄ ` 0gB;PBhIZ&F]",ra, d#+M$H 6l4g 40`c+AZIг =[#ۆ p@l$eR(]!`5̛Y00LUbr*^]1E38iC$BCATc )|\0`1`Ck \ma5e0w T2bڠ@i ALl`&dQJdcsb1Ƣ L8 20QeҌ`rA@!AI¬ vYYs@ ;+9AIW1Љ+E)8y89PIp;fJXihKEpqU(f&0%$VIOJ ZZT)5=q[b>[br/F}zGUs2켭V`1_5Dzgɟ;H-,N#gK/zrun1 >Lr<\<]MkRɲyY@`tz8'>t>/%ݧkz@1}KO)}G~|Qk "oGv:9|v~s4w;d wD@^"U)mdr[`H6)01=b>͘^Қh=7 2jeZ+zM^3:)3:j:s{Kod~ImhSf.zQtjvY/;A}Oqt@?\&X@}JUMGMN !1f 46AWiܞ/4].cWz 637M/KZA9/ikx7}o&q@3EMp9u3i(G=c L6e8Hi7˾2˔ke@E̕T TZ4*&@_%==ѝ(Pm9dy0mB6,pr2n&_@rguC6pXI˒x;t9+|:/PH2@.S^@UM $*uٮvmZP:Qwnyn {P ǚf(a7J}:ݠ}1Ѹc,G!= 8?I NYo/ˆ}\̰nV;a<75>!Wg/{xFg.=EUmD͇S]z:7\^RP\<=;{}|xN>𘪏Gtx#},lys8=O/ 28G'Ќپ]ޫRd> dl28g?O{^#{s>ٻSQ =Y3j7`Jc{QX(}p|ca6&U=/$}d AuI>czUc3C)ex\(؆}lrs=| r'm?w?8=ŷֱ\f6NNGp(~jG\ɋC Z/Cp[X} #† yp YovʼoYH_C#)CLUxOP5"AdLY H7Q WϑbUPAq!"KAJp >u#laC bܹA`cjCjMx>M+0AG&KkX{6<_נHlS dzRu ?><vVcg^+Dlnq%vY-ۄJEB<,T]p9Ux8Jꐖ3?hFTzT3_IU֣+;4)|U&YqUŜŜI}Uzs6RD拕| NeW՝R/Q>"ĠeW roLK|ȽވW{7{SD :ޔב{{?Z֑mUG~ 3];LbJTņF, ͠,E\2AA_YVd)+2fE!_"[Dd[ -5 Kk@DhQb[DZ82u([uY֫ǜ<'2:/M$070e9\e)(b1 2Y),2C2Ht5DtV{;=WkYfbugEnnm,v֖Jmv̖l,f– lf㬸jk,f⬮jz⬤*j,z⬶JkjbR b(V)ź@qLq;T$cCv߲><\,*NTbt`V/K2V^̼l:+b fryI, r5o'gD5'p;Y%,KTI ,y RmY3JN .?|$B{2# >!79Y'0e} {*1ItpIw(f>a]\o?5tb{[^c6hW㫺%ϫ먐%X=ȰzbǷðk lĹ%XBn1ؓֆ=mumUS@ 3LHQp%eJ"4t6bKŊhȎb";:Î옔3yz6*HMHN4q2Ar29zF][mqM;eP-dPS(S"2K2NT=JJ{2dGt~g}èk endstream endobj 938 0 obj << /Producer (pdfTeX-1.40.20) /Author(\376\377\0002\0000\0002\0002\000,\000\040\000J\000U\000B\000E\000\040\000D\000e\000v\000e\000l\000o\000p\000e\000r\000\040\000T\000e\000a\000m\000,\000,\000\040\000F\000o\000r\000s\000c\000h\000u\000n\000g\000s\000z\000e\000n\000t\000r\000u\000m\000\040\000J\000\374\000l\000i\000c\000h\000\040\000G\000m\000b\000H)/Title(\376\377\000J\000U\000B\000E\000\040\000D\000o\000c\000u\000m\000e\000n\000t\000a\000t\000i\000o\000n)/Subject()/Creator(LaTeX with hyperref)/Keywords() /CreationDate (D:20220824102353+02'00') /ModDate (D:20220824102353+02'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.14159265-2.6-1.40.20 (TeX Live 2019/Debian) kpathsea version 6.3.1) >> endobj 907 0 obj << /Type /ObjStm /N 31 /First 270 /Length 1263 /Filter /FlateDecode >> stream xڝI7<dĥ0dnq NN[0K>ߧEpn9TM~|,IJ+Io哕^ъ|O<*8I26<e(҆MT&$H^01i4(XڱVYu딍 ,lr&s7,_q%)b7rLF"Sg @\~fQvQv}<ԧ^_7'=E$Q*OjjAKH_Jrپ;>MQnV^OT7ԤDS%JT jgkL&[+~'4(h5Ğ U5(P=x(M'Z=i#\CQ `bOإA쉦&bObԦF;]xzbOj K{F\'8):tQNӏ7mx 6ۏƑ'6* HG4pCHiWC4t<zp^u c6zJnrWW  %l8hpXuDXzBkS?FL̝6z&`ma-^EkyN.diWsit? 1 0 "7=9&zxT:t&b/)848T*> NM-epL>bMol޴"a6oLef.ˏ22V9*^Ӌ($ G}bS77.uzJ*< PWEg )3rFadx6~mRsETuayQ`FP x ^FSGyLyոЫ`Zqs"5*C r;Z"2ρKEӆ/l@yQk\Rh{^E3a_}fp\i`ĒKWF"z"\y1K=ryd=?}<￝w 7å q<.;?2zcx+mX.N\j)rIe㗯nk\J6Š_۽Q\\j [{v&}%X]-.wzv=Nȗ{U󟗃?nD$%/1ǧrYfWtgv铖ۻo]{~x9 <31F7C3696FF0F77B50ED2BC5E3B6052E>] /Length 2265 /Filter /FlateDecode >> stream x%]lUY׷iKh)-J-JK @ @i?8Htp6gYjoьbn 3F x'{>콾w}[9>1\\̥n-HЖnv E["-V*Mՠ&qVE[ nցzf;@^k;A#ay& m H4D#lhf 6+hW v46eN6CxtUMzЍAЃ9HxFۍvA"G:hhq3oE;vp DvpL]F i4͟nїAvPt^/36JvMIFFc$c)^KɻVvPCFӔ4h5hC2u4yW).)o2"N4BZ B-eV4q;hʯ,6& u>,b1AcM hѺ YMK=Z],^m'*-5*~fWKX5wJ3i<7A6{l, c3`9V"m 6,Ӹ8^DhFfq wJ]3h(o8 As[q>vsQ^F: &8 mZ'&,%w4p yhw&}o-QeM+[ Β HyjBWc3w2F,כ}eBʙ"o4{yE`_o. _'kpXiNaGO4r5=f앆r5GAٻs\voIه͞*6'>&>rmf\ٱO>iI.sr8'?gIo4z\Ӗ4JW9~ɯZY0@ђgڤHmpӒ/hT_$_d9&ذnimr%,>жX䏺,Y?PvPiɧ礩ԍ:K>[[v4,yU͠Yߒ$,y`Y}"\q0-{8'('`’~O$8΃)0 J~n.K@M֌%?Ҩ+_5ƫn]jnK~uMP(MPY:G <2PJS*zbBҞjܒ?QXpD5`'_-Bh$>4Z!JP}2x#:,Mè#B;j>0<'T&#¨\&8"# x#`fX%|A a`X `Uu :,:As 3~ AW P'4팧jp<+ӤzTO|;GOA-<YOi=7O;E+vKFk Z{O=}Q={A5ap p#{~ò'p8K` fAtX:dz9^ZJIa=E#"XTOW¯X:ok՚gtu?պooj 键ZlY%]mZjY٧Rg7Ьa]%V֯Dm鸩gwK:)@GZa_*:b֬k_H3،Ϙplc3 f86ñplc3 f86ñplc3 f86ÓǾʞr3*t endstream endobj startxref 358842 %%EOF ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7525332 JUBE-2.5.1/examples/0000755000175000017500000000000000000000000015265 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.789493 JUBE-2.5.1/examples/cycle/0000755000175000017500000000000000000000000016364 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/cycle/cycle.xml0000644000175000017500000000046700000000000020214 0ustar00sierrousierrou00000000000000 A cycle example echo $jube_wp_cycle touch done ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/cycle/cycle.yaml0000644000175000017500000000043300000000000020347 0ustar00sierrousierrou00000000000000--- 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7914908 JUBE-2.5.1/examples/dependencies/0000755000175000017500000000000000000000000017713 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/dependencies/dependencies.xml0000644000175000017500000000125100000000000023062 0ustar00sierrousierrou00000000000000 A Dependency example 1,2,4 param_set echo $number cat first_step/stdout ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/dependencies/dependencies.yaml0000644000175000017500000000071500000000000023230 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7984848 JUBE-2.5.1/examples/do_log/0000755000175000017500000000000000000000000016530 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/do_log.xml0000644000175000017500000000072000000000000020514 0ustar00sierrousierrou00000000000000 1,2,3,4,5 param_set cp ../../../../loreipsum${number} shared grep -r -l "Hidden!" loreipsum* ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/do_log.yaml0000644000175000017500000000056300000000000020663 0ustar00sierrousierrou00000000000000- 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*"} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/loreipsum10000644000175000017500000000067600000000000020564 0ustar00sierrousierrou00000000000000Lorem 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/loreipsum20000644000175000017500000000067600000000000020565 0ustar00sierrousierrou00000000000000Lorem 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/loreipsum30000644000175000017500000000067600000000000020566 0ustar00sierrousierrou00000000000000Lorem 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/loreipsum40000644000175000017500000000070600000000000020561 0ustar00sierrousierrou00000000000000Lorem 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/do_log/loreipsum50000644000175000017500000000067600000000000020570 0ustar00sierrousierrou00000000000000Lorem 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8014815 JUBE-2.5.1/examples/duplicate/0000755000175000017500000000000000000000000017237 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/duplicate/duplicate.xml0000644000175000017500000000130500000000000021732 0ustar00sierrousierrou00000000000000 parameter duplicate example 1 2,3,4 20,30,40 int(${iterations}*(${iterations}+1)/2) options,result echo $sum ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/duplicate/duplicate.yaml0000644000175000017500000000076100000000000022101 0ustar00sierrousierrou00000000000000name: 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" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.806561 JUBE-2.5.1/examples/environment/0000755000175000017500000000000000000000000017631 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/environment/environment.xml0000644000175000017500000000147400000000000022725 0ustar00sierrousierrou00000000000000 An environment handling example VALUE export SHELL_VAR=Hello echo "$$SHELL_VAR world" param_set echo $$EXPORT_ME echo "$$SHELL_VAR again" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/environment/environment.yaml0000644000175000017500000000110200000000000023053 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8094726 JUBE-2.5.1/examples/files_and_sub/0000755000175000017500000000000000000000000020062 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/files_and_sub/file.in0000644000175000017500000000002100000000000021322 0ustar00sierrousierrou00000000000000Number: #NUMBER# ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/files_and_sub/files_and_sub.xml0000644000175000017500000000170400000000000023403 0ustar00sierrousierrou00000000000000 A file copy and substitution example 1,2,4 file.in param_set files substitute cat file.out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/files_and_sub/files_and_sub.yaml0000644000175000017500000000115500000000000023545 0ustar00sierrousierrou00000000000000name: 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 #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 #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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8114712 JUBE-2.5.1/examples/hello_world/0000755000175000017500000000000000000000000017577 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/hello_world/hello_world.xml0000644000175000017500000000077400000000000022643 0ustar00sierrousierrou00000000000000 A simple hello world Hello World hello_parameter echo $hello_str ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/hello_world/hello_world.yaml0000644000175000017500000000044100000000000022774 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8154674 JUBE-2.5.1/examples/include/0000755000175000017500000000000000000000000016710 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/include/include_data.xml0000644000175000017500000000052200000000000022045 0ustar00sierrousierrou00000000000000 1,2,4 Hello echo Test echo $number ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/include/include_data.yaml0000644000175000017500000000027000000000000022207 0ustar00sierrousierrou00000000000000parameterset: - name: param_set parameter: {name: number, type: int, _: "1,2,4"} - name: param_set2 parameter: {name: text, _: Hello} dos: - echo Test - echo $number ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/include/main.xml0000644000175000017500000000133000000000000020353 0ustar00sierrousierrou00000000000000 A include example bar param_set param_set2 echo $foo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/include/main.yaml0000644000175000017500000000074100000000000020522 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8194637 JUBE-2.5.1/examples/iterations/0000755000175000017500000000000000000000000017446 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/iterations/iterations.xml0000644000175000017500000000253400000000000022355 0ustar00sierrousierrou00000000000000 A Iteration example 1,2,4 $foo iter:$jube_wp_iteration param_set echo $bar echo $bar analyse analyse_no_reduce

jube_res_analyser jube_wp_id_first_step jube_wp_id jube_wp_iteration_first_step jube_wp_iteration foo
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/iterations/iterations.yaml0000644000175000017500000000163600000000000022521 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.824458 JUBE-2.5.1/examples/jobsystem/0000755000175000017500000000000000000000000017304 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/jobsystem/job.run.in0000644000175000017500000000034500000000000021213 0ustar00sierrousierrou00000000000000#!/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# ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/jobsystem/jobsystem.xml0000644000175000017500000000376200000000000022055 0ustar00sierrousierrou00000000000000 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/jobsystem/jobsystem.yaml0000644000175000017500000000313300000000000022207 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8265128 JUBE-2.5.1/examples/parallel_workpackages/0000755000175000017500000000000000000000000021622 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parallel_workpackages/parallel_workpackages.xml0000644000175000017500000000107500000000000026704 0ustar00sierrousierrou00000000000000 A parallel workpackages demo ",".join([ str(i) for i in range(0,10)]) param_set echo "${i}" N=1000000 ; a=1 ; for (( k = 0 ; k < $N ; ++k )); do a=$(( 2*k + 1 + $a )) ; done ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parallel_workpackages/parallel_workpackages.yaml0000644000175000017500000000062500000000000027046 0ustar00sierrousierrou00000000000000name: 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 ; for (( k = 0 ; k < $N ; ++k )); do a=$(( 2*k + 1 + $a )) ; done" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8304524 JUBE-2.5.1/examples/parameter_dependencies/0000755000175000017500000000000000000000000021753 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_dependencies/include_file.xml0000644000175000017500000000043600000000000025122 0ustar00sierrousierrou00000000000000 10 20 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_dependencies/include_file.yaml0000644000175000017500000000025100000000000025257 0ustar00sierrousierrou00000000000000parameterset: - name: depend_param_set0 parameter: {name: number2, type: int, _: 10} - name: depend_param_set1 parameter: {name: number2, type: int, _: 20} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_dependencies/parameter_dependencies.xml0000644000175000017500000000177300000000000027173 0ustar00sierrousierrou00000000000000 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" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_dependencies/parameter_dependencies.yaml0000644000175000017500000000157200000000000027332 0ustar00sierrousierrou00000000000000name: 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" ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1661329994.83245 JUBE-2.5.1/examples/parameter_update/0000755000175000017500000000000000000000000020607 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_update/parameter_update.xml0000755000175000017500000000206500000000000024661 0ustar00sierrousierrou00000000000000 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameter_update/parameter_update.yaml0000644000175000017500000000135000000000000025014 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8374677 JUBE-2.5.1/examples/parameterspace/0000755000175000017500000000000000000000000020261 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameterspace/parameterspace.xml0000644000175000017500000000121200000000000023773 0ustar00sierrousierrou00000000000000 A parameterspace example 1,2,4 Hello;World param_set echo "$text $number" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/parameterspace/parameterspace.yaml0000644000175000017500000000067700000000000024153 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8394427 JUBE-2.5.1/examples/result_creation/0000755000175000017500000000000000000000000020467 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/result_creation/result_creation.xml0000644000175000017500000000234400000000000024416 0ustar00sierrousierrou00000000000000 A result creation example 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse numbernumber_pat
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/result_creation/result_creation.yaml0000644000175000017500000000152400000000000024557 0ustar00sierrousierrou00000000000000name: 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 pattern: {name: number_pat, type: int, _: "Number: $jube_pat_int"} # ":" must be quoted #Operation step: name: write_number use: param_set #use existing parameterset do: 'echo "Number: $number"' #shell command #Analyse analyser: name: analyse use: pattern #use existing patternset analyse: step: write_number file: stdout #file which should be scanned #Create result table result: use: analyse #use existing analyser table: name: result style: pretty sort: number column: [number,number_pat] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.843449 JUBE-2.5.1/examples/result_database/0000755000175000017500000000000000000000000020427 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/result_database/result_database.xml0000644000175000017500000000222300000000000024312 0ustar00sierrousierrou00000000000000 result database creation 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse number number_pat ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/result_database/result_database.yaml0000644000175000017500000000146200000000000024460 0ustar00sierrousierrou00000000000000name: 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 # a database file "result_database.dat" is created in the current working directory name: results file: result_database.dat primekeys: "number,number_pat" key: - number - number_pat ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/result_database/result_database_filter.xml0000644000175000017500000000243000000000000025657 0ustar00sierrousierrou00000000000000 result database creation 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse number number_pat ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8454356 JUBE-2.5.1/examples/scripting_parameter/0000755000175000017500000000000000000000000021327 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/scripting_parameter/scripting_parameter.xml0000644000175000017500000000213200000000000026111 0ustar00sierrousierrou00000000000000 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" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/scripting_parameter/scripting_parameter.yaml0000644000175000017500000000137300000000000026261 0ustar00sierrousierrou00000000000000name: 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"' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8484342 JUBE-2.5.1/examples/scripting_pattern/0000755000175000017500000000000000000000000021024 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/scripting_pattern/scripting_pattern.xml0000644000175000017500000000371400000000000025312 0ustar00sierrousierrou00000000000000 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
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/scripting_pattern/scripting_pattern.yaml0000644000175000017500000000242600000000000025453 0ustar00sierrousierrou00000000000000name: 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] ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1661329994.85246 JUBE-2.5.1/examples/shared/0000755000175000017500000000000000000000000016533 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/shared/shared.xml0000644000175000017500000000114400000000000020523 0ustar00sierrousierrou00000000000000 A shared folder example 1,2,4 param_set echo $jube_wp_id >> shared/all_ids cat all_ids ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/shared/shared.yaml0000644000175000017500000000060300000000000020664 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8544264 JUBE-2.5.1/examples/statistic/0000755000175000017500000000000000000000000017274 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/statistic/statistic.xml0000644000175000017500000000273000000000000022027 0ustar00sierrousierrou00000000000000 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
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/statistic/statistic.yaml0000644000175000017500000000165300000000000022174 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8574233 JUBE-2.5.1/examples/tagging/0000755000175000017500000000000000000000000016705 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/tagging/tagging.xml0000644000175000017500000000121200000000000021043 0ustar00sierrousierrou00000000000000 Tags as logical combination Hello Hallo World param_set echo '$hello_str $world_str' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/tagging/tagging.yaml0000644000175000017500000000062600000000000021215 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8604205 JUBE-2.5.1/examples/yaml/0000755000175000017500000000000000000000000016227 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/yaml/hello_world.yaml0000644000175000017500000000072600000000000021432 0ustar00sierrousierrou00000000000000benchmark: # 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/examples/yaml/special_values.yaml0000644000175000017500000000116500000000000022115 0ustar00sierrousierrou00000000000000name: 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8873937 JUBE-2.5.1/jube2/0000755000175000017500000000000000000000000014456 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/__init__.py0000644000175000017500000000143500000000000016572 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2 package""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/analyser.py0000644000175000017500000005500500000000000016653 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Analyser class handles the analyse process""" from __future__ import (print_function, unicode_literals, division) import xml.etree.ElementTree as ET import jube2.log import os import re import glob import math import jube2.pattern import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class Analyser(object): """The Analyser handles the analyse process and store all important data to run a new analyse.""" class AnalyseFile(object): """A file which should be analysed""" def __init__(self, path): self._path = path self._use = set() def add_uses(self, use_names): """Add an addtional patternset name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) def __eq__(self, other): result = len(self._use.symmetric_difference(other.use)) == 0 return result and (self._path == other.path) def __repr__(self): return "AnalyseFile({0})".format(self._path) @property def use(self): """Return uses""" return self._use @property def path(self): """Get file path""" return self._path def etree_repr(self): """Return etree object representation""" file_etree = ET.Element("file") file_etree.text = self._path if len(self._use) > 0: file_etree.attrib["use"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._use) return file_etree def __init__(self, name, reduce_iteration=True): self._name = name self._use = set() self._analyse = dict() self._benchmark = None self._analyse_result = None self._reduce_iteration = reduce_iteration @property def benchmark(self): """Get benchmark information""" return self._benchmark @benchmark.setter def benchmark(self, benchmark): """Set benchmark information""" self._benchmark = benchmark @property def use(self): """Return uses""" return self._use @property def analyser(self): """Return analyse dict""" return self._analyse @property def analyse_result(self): """Return analyse result""" return self._analyse_result @analyse_result.setter def analyse_result(self, analyse_result): """Set analyse result""" self._analyse_result = analyse_result def add_analyse(self, step_name, analyse_file): """Add an addtional analyse file""" if step_name not in self._analyse: self._analyse[step_name] = list() if (analyse_file not in self._analyse[step_name]) and \ (analyse_file is not None): self._analyse[step_name].append(analyse_file) def add_uses(self, use_names): """Add an addtional patternset name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) @property def name(self): """Get analyser name""" return self._name def etree_repr(self): """Return etree object representation""" analyser_etree = ET.Element("analyser") analyser_etree.attrib["name"] = self._name analyser_etree.attrib["reduce"] = str(self._reduce_iteration) for use in self._use: use_etree = ET.SubElement(analyser_etree, "use") use_etree.text = use for step_name in self._analyse: analyse_etree = ET.SubElement(analyser_etree, "analyse") analyse_etree.attrib["step"] = step_name for fileobj in self._analyse[step_name]: analyse_etree.append(fileobj.etree_repr()) return analyser_etree def _combine_and_check_patternsets(self, patternset, uses): """Combine patternsets given by uses and check compatibility""" for use in uses: if use not in self._benchmark.patternsets: raise RuntimeError((" used but not " + "found").format(use)) if not patternset.is_compatible(self._benchmark.patternsets[use]): incompatible_names = patternset.get_incompatible_pattern( self._benchmark.patternsets[use]) raise RuntimeError(("Cannot use patternset \"{0}\" " + "in analyser \"{1}\", because there are " + "incompatible pattern name combinations: " "{2}") .format(use, self._name, ",".join(incompatible_names))) patternset.add_patternset(self._benchmark.patternsets[use]) def analyse(self): """Run the analyser""" LOGGER.debug("Run analyser \"{0}\"".format(self._name)) if self._benchmark is None: raise RuntimeError("No benchmark found using analyser {0}" .format(self._name)) result = dict() # Combine all patternsets patternset = jube2.pattern.Patternset() self._combine_and_check_patternsets(patternset, self._use) # Print debug info debugstr = " available pattern:\n" debugstr += \ jube2.util.output.text_table( [("pattern", "value")] + sorted([(par.name, par.value) for par in patternset.pattern_storage]), use_header_line=True, indent=9, align_right=False) debugstr += "\n available derived pattern:\n" debugstr += \ jube2.util.output.text_table( [("pattern", "value")] + sorted([(par.name, par.value) for par in patternset.derived_pattern_storage]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) for stepname in self._analyse: result[stepname] = dict() LOGGER.debug(" analyse step \"{0}\"".format(stepname)) if stepname not in self._benchmark.steps: raise RuntimeError(("Could not find " "when using analyser \"{1}\"").format( stepname, self._name)) step = self._benchmark.steps[stepname] workpackages = set(self._benchmark.workpackages[stepname]) while len(workpackages) > 0: root_workpackage = workpackages.pop() match_dict = dict() # Global patternset to store all existing pattern (e.g. from # individual file uses), necessary to evaluate default pattern # and derived pattern global_patternset = patternset.copy() result[stepname][root_workpackage.id] = dict() # Should multiple iterations be reduced to a single result line if self._reduce_iteration: siblings = set(root_workpackage.iteration_siblings) else: siblings = set([root_workpackage]) while len(siblings) > 0: workpackage = siblings.pop() if workpackage in workpackages: workpackages.remove(workpackage) # Ignore workpackages not started yet if not workpackage.started: continue parameter = \ dict([[par.name, par.value] for par in workpackage.parameterset. constant_parameter_dict.values()]) for file_obj in self._analyse[stepname]: if step.alt_work_dir is not None: file_path = step.alt_work_dir file_path = jube2.util.util.substitution( file_path, parameter) file_path = \ os.path.expandvars( os.path.expanduser(file_path)) file_path = os.path.join( self._benchmark.file_path_ref, file_path) else: file_path = workpackage.work_dir filename = \ jube2.util.util.substitution(file_obj.path, parameter) filename = \ os.path.expandvars(os.path.expanduser(filename)) file_path = os.path.join(file_path, filename) for path in glob.glob(file_path): # scan files LOGGER.debug((" scan file {0}").format(path)) new_result_dict, match_dict = \ self._analyse_file(path, patternset, global_patternset, workpackage.parameterset, match_dict, file_obj.use) result[stepname][root_workpackage.id].update( new_result_dict) # Set default pattern values if available and necessary new_result_dict = result[stepname][root_workpackage.id] for pattern in global_patternset.pattern_storage: if (pattern.default_value is not None) and \ (pattern.name not in new_result_dict): default = pattern.default_value # Convert default value if pattern.content_type == "int": if default == "nan": default = float("nan") else: default = int(float(default)) elif pattern.content_type == "float": default = float(default) new_result_dict[pattern.name] = default new_result_dict[pattern.name + "_cnt"] = 0 new_result_dict[pattern.name + "_first"] = default new_result_dict[pattern.name + "_last"] = default if pattern.content_type in ["int", "float"]: new_result_dict.update( {pattern.name + "_sum": default, pattern.name + "_min": default, pattern.name + "_max": default, pattern.name + "_avg": default, pattern.name + "_sum2": default ** 2, pattern.name + "_std": 0}) # Evaluate derived pattern new_result_dict = self._eval_derived_pattern( global_patternset, root_workpackage.parameterset, result[stepname][root_workpackage.id]) result[stepname][root_workpackage.id].update( new_result_dict) self._analyse_result = result def _eval_derived_pattern(self, patternset, parameterset, result_dict): """Evaluate all derived pattern in patternset using parameterset and result_dict""" resultset = jube2.parameter.Parameterset() for name in result_dict: resultset.add_parameter( jube2.parameter.Parameter.create_parameter( name, value=str(result_dict[name]))) # Get jube patternset jube_pattern = jube2.pattern.get_jube_pattern() # calculate derived pattern patternset.derived_pattern_substitution( [parameterset, resultset, jube_pattern.pattern_storage]) new_result_dict = dict() # Convert content type for par in patternset.derived_pattern_storage: if par.mode not in jube2.conf.ALLOWED_SCRIPTTYPES: new_result_dict[par.name] = \ jube2.util.util.convert_type(par.content_type, par.value, stop=False) return new_result_dict def _analyse_file(self, file_path, patternset, global_patternset, parameterset, match_dict=None, additional_uses=None): """Scan given files with given pattern and produce a result parameterset""" if additional_uses is None: additional_uses = set() if match_dict is None: match_dict = dict() if not os.path.isfile(file_path): return dict(), match_dict local_patternset = patternset.copy() # Add file specific uses self._combine_and_check_patternsets(local_patternset, additional_uses) self._combine_and_check_patternsets(global_patternset, additional_uses) # Unique pattern/parameter check if (not parameterset.is_compatible( local_patternset.pattern_storage)) or \ (not parameterset.is_compatible( local_patternset.derived_pattern_storage)): incompatible_names = parameterset.get_incompatible_parameter( local_patternset.pattern_storage) incompatible_names.update(parameterset.get_incompatible_parameter( local_patternset.derived_pattern_storage)) raise RuntimeError(("A pattern and a parameter (\"{0}\") " "using the same name in " "analyser \"{1}\"").format( ",".join(incompatible_names), self._name)) # Get jube patternset jube_pattern = jube2.pattern.get_jube_pattern() # Do pattern substitution local_patternset.pattern_substitution( [parameterset, jube_pattern.pattern_storage]) patternlist = [p for p in local_patternset.pattern_storage] file_handle = open(file_path, "r") # Read file content data = file_handle.read() for pattern in patternlist: if pattern.name not in match_dict: match_dict[pattern.name] = dict() try: mode = re.MULTILINE if pattern.dotall: mode += re.DOTALL regex = re.compile(pattern.value, mode) except re.error as ree: raise RuntimeError(("Error inside pattern \"{0}\" : " + "\"{1}\" : {2}") .format(pattern.name, pattern.value, ree)) # Run regular expression matches = re.findall(regex, data) # If there are different groups reduce result shape if regex.groups > 1: match_list = list() for match in matches: match_list = match_list + list(match) else: match_list = matches # Remove empty matches match_list = [match for match in match_list if match != ""] # Convert to pattern type new_match_list = list() for match in match_list: try: if pattern.content_type == "int": if match == "nan": new_match_list.append(float("nan")) else: new_match_list.append(int(float(match))) elif pattern.content_type == "float": new_match_list.append(float(match)) else: new_match_list.append(match) except ValueError: LOGGER.warning(("\"{0}\" cannot be represented " + "as a \"{1}\"") .format(match, pattern.content_type)) match_list = new_match_list if len(match_list) > 0: # First match is default if "first" not in match_dict[pattern.name]: match_dict[pattern.name]["first"] = match_list[0] for match in match_list: if pattern.content_type in ["int", "float"]: if "min" in match_dict[pattern.name]: match_dict[pattern.name]["min"] = \ min(match_dict[pattern.name]["min"], match) else: match_dict[pattern.name]["min"] = match if "max" in match_dict[pattern.name]: match_dict[pattern.name]["max"] = \ max(match_dict[pattern.name]["max"], match) else: match_dict[pattern.name]["max"] = match if "sum" in match_dict[pattern.name]: match_dict[pattern.name]["sum"] += match else: match_dict[pattern.name]["sum"] = match try: if "sum2" in match_dict[pattern.name]: match_dict[pattern.name]["sum2"] += match ** 2 else: match_dict[pattern.name]["sum2"] = match ** 2 except OverflowError: LOGGER.warning( "Squared sum cannot be represented, " + "numerical result out of range.") match_dict[pattern.name]["sum2"] = math.nan if "cnt" in match_dict[pattern.name]: match_dict[pattern.name]["cnt"] += 1 else: match_dict[pattern.name]["cnt"] = 1 if pattern.content_type in ["int", "float"]: if match_dict[pattern.name]["cnt"] > 0: match_dict[pattern.name]["avg"] = \ (match_dict[pattern.name]["sum"] / match_dict[pattern.name]["cnt"]) if match_dict[pattern.name]["cnt"] > 1: try: match_dict[pattern.name]["std"] = math.sqrt( (abs(match_dict[pattern.name]["sum2"] - (match_dict[pattern.name]["sum"] ** 2 / match_dict[pattern.name]["cnt"])) / (match_dict[pattern.name]["cnt"] - 1))) except OverflowError: match_dict[pattern.name]["std"] = 0 else: match_dict[pattern.name]["std"] = 0 match_dict[pattern.name]["last"] = match_list[-1] info_str = " file \"{0}\" scanned pattern found:\n".format( os.path.basename(file_path)) info_str += jube2.util.output.text_table( [(_name, ", ".join(["{0}:{1}".format(key, con) for key, con in value.items()])) for _name, value in match_dict.items()], indent=9, align_right=True, auto_linebreak=True) LOGGER.debug(info_str) file_handle.close() # Create result dict result_dict = dict() for pattern_name in match_dict: for option in match_dict[pattern_name]: if option == "first": result_dict[pattern_name] = \ match_dict[pattern_name][option] name = "{0}_{1}".format(pattern_name, option) result_dict[name] = match_dict[pattern_name][option] return result_dict, match_dict def analyse_etree_repr(self): """Create an etree representation of a analyse dict: stepname -> workpackage_id -> filename -> patternname -> value """ etree = list() if self._analyse_result is None: return etree for stepname in self._analyse_result: step_etree = ET.Element("step") step_etree.attrib["name"] = stepname for workpackage_id in self._analyse_result[stepname]: workpackage_etree = ET.SubElement(step_etree, "workpackage") workpackage_etree.attrib["id"] = str(workpackage_id) for pattern in self._analyse_result[stepname][workpackage_id]: if type(self._analyse_result[stepname][workpackage_id] [pattern]) is int: content_type = "int" elif type(self._analyse_result[stepname][ workpackage_id][pattern]) is float: content_type = "float" else: content_type = "string" pattern_etree = ET.SubElement(workpackage_etree, "pattern") pattern_etree.attrib["name"] = pattern pattern_etree.attrib["type"] = content_type pattern_etree.text = \ str(self._analyse_result[stepname][workpackage_id] [pattern]) etree.append(step_etree) return etree ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/benchmark.py0000644000175000017500000010340000000000000016760 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Benchmark class manages the benchmark process""" from __future__ import (print_function, unicode_literals, division) import multiprocessing as mp import xml.etree.ElementTree as ET import xml.dom.minidom as DOM import logging import os import re import stat import pprint import shutil import itertools import jube2.parameter import jube2.util.util import jube2.util.output import jube2.conf import jube2.log LOGGER = jube2.log.get_logger(__name__) class Benchmark(object): """The Benchmark class contains all data to run a benchmark""" def __init__(self, name, outpath, parametersets, substitutesets, filesets, patternsets, steps, analyser, results, results_order, comment="", tags=None, file_path_ref="."): self._name = name self._outpath = outpath self._parametersets = parametersets self._substitutesets = substitutesets self._filesets = filesets self._patternsets = patternsets self._steps = steps self._analyser = analyser for analyser in self._analyser.values(): analyser.benchmark = self self._results = results self._results_order = results_order for result in self._results.values(): result.benchmark = self self._workpackages = dict() self._work_stat = jube2.util.util.WorkStat() self._comment = comment self._id = -1 self._file_path_ref = file_path_ref if tags is None: self._tags = set() else: self._tags = tags @property def name(self): """Return benchmark name""" return self._name @property def comment(self): """Return comment string""" return self._comment @property def tags(self): """Return set of tags""" return self._tags @comment.setter def comment(self, new_comment): """Set new comment string""" self._comment = new_comment @property def parametersets(self): """Return parametersets""" return self._parametersets @property def patternsets(self): """Return patternsets""" return self._patternsets @property def analyser(self): """Return analyser""" return self._analyser @property def results(self): """Return results""" return self._results @property def results_order(self): """Return results_order""" return self._results_order @property def file_path_ref(self): """Get file path reference""" return self._file_path_ref @file_path_ref.setter def file_path_ref(self, file_path_ref): """Set file path reference""" self._file_path_ref = file_path_ref @property def outpath(self): """Return outpath""" return self._outpath @outpath.setter def outpath(self, new_outpath): """Overwrite outpath""" self._outpath = new_outpath @property def substitutesets(self): """Return substitutesets""" return self._substitutesets @property def workpackages(self): """Return workpackages""" return self._workpackages def add_tags(self, other_tags): if other_tags is not None: self._tags = self._tags.union(set(other_tags)) def workpackage_by_id(self, wp_id): """Search and return a benchmark workpackage by its wp_id""" for stepname in self._workpackages: for workpackage in self._workpackages[stepname]: if workpackage.id == wp_id: return workpackage return None def remove_workpackage(self, workpackage_to_delete): """Remove a specifc workpackage""" stepname = workpackage_to_delete.step.name if stepname in self._workpackages and \ workpackage_to_delete in self._workpackages[stepname]: self._workpackages[stepname].remove(workpackage_to_delete) @property def work_stat(self): """Return work queue""" return self._work_stat @property def filesets(self): """Return filesets""" return self._filesets def delete_bench_dir(self): """Delete all data inside benchmark directory""" if os.path.exists(self.bench_dir): shutil.rmtree(self.bench_dir, ignore_errors=True) @property def steps(self): """Return steps""" return self._steps @property def workpackage_status(self): """Retun workpackage information dict""" result_dict = dict() for stepname in self._workpackages: result_dict[stepname] = {"all": 0, "open": 0, "wait": 0, "error": 0, "done": 0} for workpackage in self._workpackages[stepname]: result_dict[stepname]["all"] += 1 if workpackage.done: result_dict[stepname]["done"] += 1 elif workpackage.error: result_dict[stepname]["error"] += 1 elif workpackage.started: result_dict[stepname]["wait"] += 1 else: result_dict[stepname]["open"] += 1 return result_dict @property def benchmark_status(self): """Retun global workpackage information dict""" result_dict = {"all": 0, "open": 0, "wait": 0, "error": 0, "done": 0} for status in self.workpackage_status.values(): result_dict["all"] += status["all"] result_dict["open"] += status["open"] result_dict["wait"] += status["wait"] result_dict["error"] += status["error"] result_dict["done"] += status["done"] return result_dict @property def id(self): """Return benchmark id""" return self._id @id.setter def id(self, new_id): """Set new benchmark id""" self._id = new_id def get_jube_parameterset(self): """Return parameterset which contains benchmark related information""" parameterset = jube2.parameter.Parameterset() # benchmark id parameterset.add_parameter( jube2.parameter.Parameter. create_parameter( "jube_benchmark_id", str(self._id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # benchmark id with padding parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_padid", jube2.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) # benchmark name parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_name", self._name, update_mode=jube2.parameter.JUBE_MODE)) # benchmark home parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_home", os.path.abspath(self._file_path_ref), update_mode=jube2.parameter.JUBE_MODE)) # benchmark rundir parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_benchmark_rundir", os.path.abspath(self.bench_dir), update_mode=jube2.parameter.JUBE_MODE)) timestamps = jube2.util.util.read_timestamps( os.path.join(self.bench_dir, jube2.conf.TIMESTAMPS_INFO)) # benchmark start parameterset.add_parameter( jube2.parameter.Parameter.create_parameter( "jube_benchmark_start", timestamps.get("start", "").replace(" ", "T"), update_mode=jube2.parameter.JUBE_MODE)) return parameterset def etree_repr(self, new_cwd=None): """Return etree object representation""" benchmark_etree = ET.Element("benchmark") if len(self._comment) > 0: comment_element = ET.SubElement(benchmark_etree, "comment") comment_element.text = self._comment benchmark_etree.attrib["name"] = self._name # Modify file_path_ref and outpath to be relativly correct towards # new configuration file position if new_cwd is not None: benchmark_etree.attrib["file_path_ref"] = \ os.path.relpath(self._file_path_ref, new_cwd) if not os.path.isabs(self._outpath): benchmark_etree.attrib["outpath"] = \ os.path.relpath(self._outpath, new_cwd) else: benchmark_etree.attrib["outpath"] = self._outpath for parameterset in self._parametersets.values(): benchmark_etree.append(parameterset.etree_repr()) for substituteset in self._substitutesets.values(): benchmark_etree.append(substituteset.etree_repr()) for fileset in self._filesets.values(): benchmark_etree.append(fileset.etree_repr()) for patternset in self._patternsets.values(): benchmark_etree.append(patternset.etree_repr()) for step in self._steps.values(): benchmark_etree.append(step.etree_repr()) for analyser in self._analyser.values(): benchmark_etree.append(analyser.etree_repr()) for result_name in self._results_order: result = self._results[result_name] benchmark_etree.append(result.etree_repr()) return benchmark_etree def __repr__(self): return pprint.pformat(self.__dict__) def _create_initial_workpackages(self): """Create initial workpackages of current benchmark and create graph structure.""" self._workpackages = dict() self._work_stat = jube2.util.util.WorkStat() # Create workpackage storage for step_name in self._steps: self._workpackages[step_name] = list() # Create initial workpackages for step in self._steps.values(): if len(step.depend) == 0: new_workpackages = \ self._create_new_workpackages_with_parents(step) self._workpackages[step.name] += new_workpackages for workpackage in new_workpackages: workpackage.queued = True self._work_stat.put(workpackage) def analyse(self, show_info=True, specific_analyser_name=None): """Run analyser""" if show_info: LOGGER.info(">>> Start analyse") if specific_analyser_name is not None and \ specific_analyser_name in self._analyser: self._analyser[specific_analyser_name].analyse() else: for analyser in self._analyser.values(): analyser.analyse() if ((not jube2.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_analyse_data(os.path.join(self.bench_dir, jube2.conf.ANALYSE_FILENAME)) if show_info: LOGGER.info(">>> Analyse finished") def create_result(self, only=None, show=False, data_list=None, style=None): """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) if result.result_dir is None: result_dir = os.path.join(self.bench_dir, jube2.conf.RESULT_DIRNAME) else: result_dir = result.result_dir result_dir = os.path.expanduser(result_dir) result_dir = os.path.expandvars(result_dir) result_dir = jube2.util.util.id_dir( os.path.join(self.file_path_ref, result_dir), self.id) if (not os.path.exists(result_dir)) and \ (not jube2.conf.DEBUG_MODE): try: os.makedirs(result_dir) except OSError: pass if ((not jube2.conf.DEBUG_MODE) and (os.path.exists(result_dir)) and (os.access(result_dir, os.W_OK))): filename = os.path.join(result_dir, "{0}.dat".format(result.name)) else: filename = None result_data.create_result(show=show, filename=filename) if result_data in data_list: data_list[data_list.index(result_data)].add_result_data( result_data) else: data_list.append(result_data) return data_list def update_analyse_and_result(self, new_patternsets, new_analyser, new_results, new_results_order, new_cwd): """Update analyser and result data""" if os.path.exists(self.bench_dir): LOGGER.debug("Update analyse and result data") self._patternsets = new_patternsets old_analyser = self._analyser self._analyser = new_analyser self._results = new_results self._results_order = new_results_order for analyser in self._analyser.values(): if analyser.name in old_analyser: analyser.analyse_result = \ old_analyser[analyser.name].analyse_result analyser.benchmark = self for result in self._results.values(): result.benchmark = self # change result dir position relative to cwd if (result.result_dir is not None) and \ (new_cwd is not None) and \ (not os.path.isabs(result.result_dir)): result.result_dir = \ os.path.join(new_cwd, result.result_dir) if ((not jube2.conf.DEBUG_MODE) and (os.access(self.bench_dir, os.W_OK))): self.write_benchmark_configuration( os.path.join(self.bench_dir, jube2.conf.CONFIGURATION_FILENAME), outpath="..") def write_analyse_data(self, filename): """All analyse data will be written to given file using xml representation""" # Create root-tag and append analyser analyse_etree = ET.Element("analyse") for analyser_name in self._analyser: analyser_etree = ET.SubElement(analyse_etree, "analyser") analyser_etree.attrib["name"] = analyser_name for etree in self._analyser[analyser_name].analyse_etree_repr(): analyser_etree.append(etree) xml = jube2.util.output.element_tree_tostring( analyse_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode("UTF-8")) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def _create_new_workpackages_for_workpackage(self, workpackage): """Create and return new workpackages if given workpackage was finished.""" all_new_workpackages = list() if not workpackage.done or len(workpackage.children) > 0: return all_new_workpackages LOGGER.debug(("Create new workpackages for workpackage" " {0}({1})").format( workpackage.step.name, workpackage.id)) # Search for dependent steps dependent_steps = [step for step in self._steps.values() if workpackage.step.name in step.depend] # Search for possible workpackage parents for dependent_step in dependent_steps: parent_workpackages = [[ parent_workpackage for parent_workpackage in self._workpackages[step_name] if parent_workpackage.done] for step_name in dependent_step.depend if (step_name in self._workpackages) and (step_name != workpackage.step.name)] parent_workpackages.append([workpackage]) # Create all possible parent combinations workpackage_combinations = \ [iterator for iterator in itertools.product(*parent_workpackages)] possible_combination = len(workpackage_combinations) for workpackage_combination in workpackage_combinations: new_workpackages = self._create_new_workpackages_with_parents( dependent_step, workpackage_combination) if len(new_workpackages) > 0: possible_combination -= 1 # Create links: parent workpackages -> new children for new_workpackage in new_workpackages: for parent in workpackage_combination: parent.add_children(new_workpackage) self._workpackages[dependent_step.name] += new_workpackages all_new_workpackages += new_workpackages if possible_combination > 0: LOGGER.debug((" {0} workpackages combinations were skipped" " while checking possible parent combinations" " for step {1}").format(possible_combination, dependent_step.name)) LOGGER.debug(" {0} new workpackages created".format( len(all_new_workpackages))) return all_new_workpackages def _create_new_workpackages_with_parents(self, step, parent_workpackages=None): """Create workpackages with given parent combination""" if parent_workpackages is None: parent_workpackages = list() # Combine and check parent parametersets parameterset = jube2.parameter.Parameterset() incompatible_parameter_names = set() for parent_workpackage in parent_workpackages: # Check weather parameter combination is possible or not. # JUBE Parameter can be ignored incompatible_parameter_names = incompatible_parameter_names.union( parameterset.get_incompatible_parameter( parent_workpackage.parameterset, update_mode=jube2.parameter.JUBE_MODE)) parameterset.add_parameterset( parent_workpackage.parameterset) # Sort parent workpackges after total iteration number and name sorted_parents = list(parent_workpackages) sorted_parents.sort(key=lambda x: x.step.name) sorted_parents.sort(key=lambda x: x.step.iterations) iteration_base = 0 for i, parent in enumerate(sorted_parents): if i == 0: iteration_base = parent.iteration else: iteration_base = \ parent.step.iterations * iteration_base + parent.iteration parameterset.remove_jube_parameter() # Create new workpackages new_workpackages = step.create_workpackages( self, parameterset, iteration_base=iteration_base, parents=parent_workpackages, incompatible_parameters=incompatible_parameter_names) # Update iteration sibling connections if len(parent_workpackages) > 0 and len(new_workpackages) > 0: for sibling in parent_workpackages[0].iteration_siblings: if sibling != parent_workpackages[0]: for child in sibling.children: for workpackage in new_workpackages: if workpackage.parameterset.is_compatible( child.parameterset, update_mode=jube2.parameter.JUBE_MODE): workpackage.iteration_siblings.add(child) child.iteration_siblings.add(workpackage) return new_workpackages def new_run(self): """Create workpackage structure and run benchmark""" # Check benchmark consistency LOGGER.debug("Start consistency check") jube2.util.util.consistency_check(self) # Create benchmark directory LOGGER.debug("Create benchmark directory") self._create_bench_dir() # Change logfile jube2.log.change_logfile_name(os.path.join( self.bench_dir, jube2.conf.LOGFILE_RUN_NAME)) # Move parse logfile into benchmark folder if os.path.isfile(os.path.join(self._file_path_ref, jube2.conf.DEFAULT_LOGFILE_NAME)): shutil.move(os.path.join(self._file_path_ref, jube2.conf.DEFAULT_LOGFILE_NAME), os.path.join(self.bench_dir, jube2.conf.LOGFILE_PARSE_NAME)) # Reset Workpackage counter jube2.workpackage.Workpackage.id_counter = 0 # Create initial workpackages LOGGER.debug("Create initial workpackages") self._create_initial_workpackages() # Store workpackage information LOGGER.debug("Store initial workpackage information") self.write_workpackage_information( os.path.join(self.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) LOGGER.debug("Start benchmark run") self.run() def run(self): """Run benchmark""" title = "benchmark: {0}".format(self._name) title += "\nid: {0}".format(self._id) if jube2.conf.DEBUG_MODE: title += " ---DEBUG_MODE---" title += "\n\n{0}".format(self._comment) infostr = jube2.util.output.text_boxed(title) LOGGER.info(infostr) if not jube2.conf.HIDE_ANIMATIONS: print("\nRunning workpackages (#=done, 0=wait, E=error):") status = self.benchmark_status jube2.util.output.print_loading_bar( status["done"], status["all"], status["wait"], status["error"]) # Handle all workpackages in given order while not self._work_stat.empty(): workpackage = self._work_stat.get() run_parallel = False def collect_result(val): """used collect return values from pool.apply_async""" # run postprocessing of each wp for i, wp in enumerate(self._workpackages[val["step_name"]]): if wp.id == val["id"]: if(len(val) == 2): # workpackage is done or its execution was erroneous pass else: # update corresponding wp in self._workpackage with modified wp wp.env = val["env"] # restore the parameters containing a method of a class, # which needed to be deleted within the multiprocess # execution to avoid excessive memory usage for p in wp._parameterset.all_parameters: if(p.search_method(propertyString="eval_helper", recursiveProperty="based_on")): val["parameterset"].add_parameter(p) wp.parameterset = val["parameterset"] wp.cycle = val["cycle"] self.wp_post_run_config(wp) break def log_e(e): """used to print error_callback from pool.apply_async""" print(e) # TODO # writeXML position(y) - replace by database # TODO END if not workpackage.done: # execute wps in parallel which have the same name if workpackage.step.procs > 1: run_parallel = True procs = workpackage.step.procs name = workpackage.step.name pool = mp.Pool(processes=procs) # add wps to the parallel pool as long as they have the same name while True: pool.apply_async(workpackage.run, args=('p',), callback=collect_result, error_callback=log_e) if not self._work_stat.empty(): workpackage = self._work_stat.get() # push back as first element of _work_stat and # terminate parallel loop if workpackage.step.name != name: self._work_stat.push_back(workpackage) break else: break pool.close() pool.join() else: workpackage.run() if run_parallel == True: # merge parallel run log files into the main run log file and # delete the parallel logs log_fname = jube2.log.LOGFILE_NAME.split('/')[-1] filenames = [file for file in os.listdir(self.bench_dir) if file.startswith(log_fname.split('.')[0]) and file != log_fname] filenames.sort(key=lambda o: int(re.split('_|\.', o)[1])) with open(os.path.join(self.bench_dir, jube2.conf.LOGFILE_RUN_NAME), 'a') as outfile: for fname in filenames: with open(os.path.join(self.bench_dir, fname), 'r') as infile: contents = infile.read() outfile.write(contents) os.remove(os.path.join(self.bench_dir, fname)) run_parallel = False else: self.wp_post_run_config(workpackage) # Store workpackage information self.write_workpackage_information( os.path.join(self.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) print("\n") status_data = [("stepname", "all", "open", "wait", "error", "done")] status_data += [(stepname, str(_status["all"]), str(_status["open"]), str(_status["wait"]), str(_status["error"]), str(_status["done"])) for stepname, _status in self.workpackage_status.items()] LOGGER.info(jube2.util.output.text_table( status_data, use_header_line=True, indent=2)) LOGGER.info("\n>>>> Benchmark information and " + "further useful commands:") LOGGER.info(">>>> id: {0}".format(self._id)) LOGGER.info(">>>> handle: {0}".format(self._outpath)) LOGGER.info(">>>> dir: {0}".format(self.bench_dir)) status = self.benchmark_status if status["all"] != status["done"]: LOGGER.info((">>>> continue: jube continue {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> analyse: jube analyse {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> result: jube result {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> info: jube info {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info((">>>> log: jube log {0} " + "--id {1}").format(self._outpath, self._id)) LOGGER.info(jube2.util.output.text_line() + "\n") def wp_post_run_config(self, workpackage): """additional processing of workpackage: - update status bar - build up queue after restart """ self._create_new_workpackages_for_workpackage(workpackage) # Update queues (move waiting workpackages to work queue # if possible) self._work_stat.update_queues(workpackage) if not jube2.conf.HIDE_ANIMATIONS: status = self.benchmark_status jube2.util.output.print_loading_bar( status["done"], status["all"], status["wait"], status["error"]) workpackage.queued = False for mode in ("only_started", "all"): for child in workpackage.children: all_done = True for parent in child.parents: all_done = all_done and parent.done if all_done: if (mode == "only_started" and child.started) or \ (mode == "all" and (not child.queued)): child.queued = True self._work_stat.put(child) def _create_bench_dir(self): """Create the directory for a benchmark.""" # Get group_id if available (given by JUBE_GROUP_NAME) group_id = jube2.util.util.check_and_get_group_id() # Check if outpath exists if not (os.path.exists(self._outpath) and os.path.isdir(self._outpath)): os.makedirs(self._outpath) if group_id is not None: os.chown(self._outpath, os.getuid(), group_id) # Generate unique ID in outpath if self._id < 0: self._id = jube2.util.util.get_current_id(self._outpath) + 1 if os.path.exists(self.bench_dir): raise RuntimeError("Benchmark directory \"{0}\" already exists" .format(self.bench_dir)) os.makedirs(self.bench_dir) # If JUBE_GROUP_NAME is given, set GID-Bit and change group if group_id is not None: os.chown(self.bench_dir, os.getuid(), group_id) os.chmod(self.bench_dir, os.stat(self.bench_dir).st_mode | stat.S_ISGID) self.write_benchmark_configuration( os.path.join(self.bench_dir, jube2.conf.CONFIGURATION_FILENAME), outpath="..") jube2.util.util.update_timestamps(os.path.join( self.bench_dir, jube2.conf.TIMESTAMPS_INFO), "start", "change") def write_benchmark_configuration(self, filename, outpath=None): """The current benchmark configuration will be written to given file using xml representation""" # Create root-tag and append single benchmark benchmarks_etree = ET.Element("jube") benchmarks_etree.attrib["version"] = jube2.conf.JUBE_VERSION # Store tag information if len(self._tags) > 0: selection_etree = ET.SubElement(benchmarks_etree, "selection") for tag in self._tags: tag_etree = ET.SubElement(selection_etree, "tag") tag_etree.text = tag benchmark_etree = self.etree_repr(new_cwd=self.bench_dir) if outpath is not None: benchmark_etree.attrib["outpath"] = outpath benchmarks_etree.append(benchmark_etree) xml = jube2.util.output.element_tree_tostring( benchmarks_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode('UTF-8')) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def reset_all_workpackages(self): """Reset workpackage state""" for workpackages in self._workpackages.values(): for workpackage in workpackages: workpackage.done = False def write_workpackage_information(self, filename): """All workpackage information will be written to given file using xml representation""" # Create root-tag and append workpackages workpackages_etree = ET.Element("workpackages") for workpackages in self._workpackages.values(): for workpackage in workpackages: workpackages_etree.append(workpackage.etree_repr()) xml = jube2.util.output.element_tree_tostring( workpackages_etree, encoding="UTF-8") # Using dom for pretty-print dom = DOM.parseString(xml.encode("UTF-8")) fout = open(filename, "wb") fout.write(dom.toprettyxml(indent=" ", encoding="UTF-8")) fout.close() def set_workpackage_information(self, workpackages, work_stat): """Set new workpackage information""" self._workpackages = workpackages self._work_stat = work_stat @property def bench_dir(self): """Return benchmark directory""" return jube2.util.util.id_dir(self._outpath, self._id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/completion.py0000644000175000017500000000637500000000000017214 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Shell Completions""" from __future__ import (print_function, unicode_literals, division) import jube2.main # This is formatted once. BASH_CASE_TEMPLATE = """\ "{command}") COMPREPLY=( $(compgen -W "{opts}" -- ${{cur}}) ) return 0 ;; """ # This is formatted once. BASH_SCRIPT_TEMPLATE = """ _{command_name} () {{ local cur prev words cword comm subparsers subcom iter COMPREPLY=() words=(${{COMP_WORDS[@]}}) cword=COMP_CWORD comm=${{words[0]}} cur="${{words[cword]}}" prev="${{words[cword-1]}}" subcom="${{words[0]}}" for iter in ${{words[@]:1}}; do if [[ $iter != -* ]] && [[ " {all_subcoms} " == *" $iter "* ]]; then subcom=$iter break fi done subparsers="{subparser}" if [[ ${{cur}} == -* ]] ; then case "${{subcom}}" in {cases_sub} *) esac elif [[ ${{subcom}} == "$comm" ]] ; then COMPREPLY=( $(compgen -W "${{subparsers}}" -- ${{cur}}) ) fi }} && complete -o bashdefault -o default -F _{command_name} {command_name} """ def complete_function_bash(args): """Print completion function for bash.""" subparser = jube2.main.gen_subparser_conf() all_sub_names = " ".join(sorted(subparser)) parser = sorted([opt for opts, kwargs in jube2.main.gen_parser_conf() for opt in opts if opt.startswith("--")]) command_name = args.command_name[0] complete_options = dict() # Iterate over all subparsers for sub_name, sub in sorted(subparser.items()): if "arguments" not in sub: continue # Iterate over all their options tmp_list = [argument for key in sub["arguments"] for argument in key if argument.startswith("--")] complete_options[sub_name] = " ".join(tmp_list) cases_sub = "".join(BASH_CASE_TEMPLATE.format(command=command, opts=opts) for command, opts in sorted(complete_options.items())) cases_sub += BASH_CASE_TEMPLATE.format(command=command_name, opts=" ".join(parser)) subparser_str = " ".join(sorted(subparser.keys())) script = BASH_SCRIPT_TEMPLATE.format( subparser=subparser_str, cases_sub=cases_sub, command_name=command_name, all_subcoms=all_sub_names) print(script) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/conf.py0000644000175000017500000000465100000000000015763 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # 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.5.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/jube2/version" UPDATE_URL = "http://apps.fz-juelich.de/jsc/jube/jube2/download.php" STANDARD_SHELL = "/bin/sh" EXIT_ON_ERROR = False # input/output DEFAULT_SEPARATOR = "," ZERO_FILL_DEFAULT = 6 DEFAULT_WIDTH = 70 MAX_TABLE_CELL_WIDTH = 40 HIDE_ANIMATIONS = False VERBOSE_STDOUT_READ_CHUNK_SIZE = 50 VERBOSE_STDOUT_POLL_SLEEP = 0.05 SYSLOG_FMT_STRING = "jube[%(process)s]: %(message)s" PREPROCESS_MAX_ITERATION = 10 # filenames WORKPACKAGE_DONE_FILENAME = "done" WORKPACKAGE_ERROR_FILENAME = "error" DO_LOG_FILENAME = "do_log" CONFIGURATION_FILENAME = "configuration.xml" WORKPACKAGES_FILENAME = "workpackages.xml" ANALYSE_FILENAME = "analyse.xml" RESULT_DIRNAME = "result" ENVIRONMENT_INFO = "jube_environment_information.dat" TIMESTAMPS_INFO = "timestamps" # logging DEFAULT_LOGFILE_NAME = "jube-parse.log" LOGFILE_DEBUG_NAME = "jube-debug.log" LOGFILE_DEBUG_MODE = "w" LOGFILE_RUN_NAME = "run.log" LOGFILE_CONTINUE_NAME = "continue.log" LOGFILE_ANALYSE_NAME = "analyse.log" LOGFILE_PARSE_NAME = "parse.log" LOGFILE_RESULT_NAME = "result.log" LOG_CONSOLE_FORMAT = "%(message)s" LOG_FILE_FORMAT = "[%(asctime)s]:%(levelname)s: %(message)s" DEFAULT_LOGGING_MODE = "default" # other ERROR_MSG_LINES = 5 MAX_RECURSIVE_SUB = 5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/fileset.py0000644000175000017500000002530500000000000016470 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Fileset related classes""" from __future__ import (print_function, unicode_literals, division) import os import shutil import xml.etree.ElementTree as ET import jube2.util.util import jube2.conf import jube2.step import jube2.log import glob LOGGER = jube2.log.get_logger(__name__) class Fileset(list): """Container for file copy, link and prepare operations""" def __init__(self, name): list.__init__(self) self._name = name @property def name(self): """Return fileset name""" return self._name def etree_repr(self): """Return etree object representation""" fileset_etree = ET.Element("fileset") fileset_etree.attrib["name"] = self._name for file_handle in self: fileset_etree.append(file_handle.etree_repr()) return fileset_etree def create(self, work_dir, parameter_dict, alt_work_dir=None, environment=None, file_path_ref=""): """Copy/load/prepare all files in fileset""" for file_handle in self: if type(file_handle) is Prepare: file_handle.execute( parameter_dict=parameter_dict, work_dir=alt_work_dir if alt_work_dir is not None else work_dir, environment=environment) else: file_handle.create( work_dir=work_dir, parameter_dict=parameter_dict, alt_work_dir=alt_work_dir, file_path_ref=file_path_ref, environment=environment) class File(object): """Generic file access""" def __init__(self, path, name=None, is_internal_ref=False, active="true", source_dir="", target_dir=""): self._path = path self._source_dir = source_dir self._target_dir = target_dir self._name = name self._file_path_ref = "" self._active = active self._is_internal_ref = is_internal_ref def create(self, work_dir, parameter_dict, alt_work_dir=None, file_path_ref="", environment=None): """Create file access""" # Check active status active = jube2.util.util.eval_bool(jube2.util.util.substitution( self._active, parameter_dict)) if not active: return pathname = jube2.util.util.substitution(self._path, parameter_dict) pathname = os.path.expanduser(pathname) source_dir = jube2.util.util.substitution(self._source_dir, parameter_dict) source_dir = os.path.expanduser(source_dir) target_dir = jube2.util.util.substitution(self._target_dir, parameter_dict) target_dir = os.path.expanduser(target_dir) if environment is not None: pathname = jube2.util.util.substitution(pathname, environment) source_dir = jube2.util.util.substitution(source_dir, environment) target_dir = jube2.util.util.substitution(target_dir, environment) else: pathname = os.path.expandvars(pathname) source_dir = os.path.expandvars(source_dir) target_dir = os.path.expandvars(target_dir) # Add source prefix directory if needed pathname = os.path.join(source_dir, pathname) if self._is_internal_ref: pathname = os.path.join(work_dir, pathname) else: pathname = os.path.join(self._file_path_ref, pathname) pathname = os.path.join(file_path_ref, pathname) pathname = os.path.normpath(pathname) if self._name is None: name = os.path.basename(pathname) else: name = jube2.util.util.substitution(self._name, parameter_dict) name = os.path.expanduser(name) if environment is not None: name = jube2.util.util.substitution(name, environment) else: name = os.path.expandvars(name) if alt_work_dir is not None: work_dir = alt_work_dir # Shell expansion pathes = glob.glob(pathname) if (len(pathes) == 0) and (not jube2.conf.DEBUG_MODE): raise RuntimeError("no files found using \"{0}\"" .format(pathname)) for path in pathes: # When using shell extensions, alternative filenames are not # allowed for multiple matches. if (len(pathes) > 1) or ((pathname != path) and (name == os.path.basename(pathname))): name = os.path.basename(path) # Add target prefix directory if needed name = os.path.join(target_dir, name) new_file_path = os.path.join(work_dir, name) # Create target_dir if needed if (len(os.path.dirname(new_file_path)) > 0 and not os.path.exists(os.path.dirname(new_file_path)) and not jube2.conf.DEBUG_MODE): os.makedirs(os.path.dirname(new_file_path)) self.create_action(path, name, new_file_path) def create_action(self, path, name, new_file_path): """File access type specific creation""" raise NotImplementedError() def etree_repr(self): """Return etree object representation""" raise NotImplementedError() @property def path(self): """Return filepath""" return self._path @property def file_path_ref(self): """Get file path reference""" return self._file_path_ref @file_path_ref.setter def file_path_ref(self, file_path_ref): """Set file path reference""" self._file_path_ref = file_path_ref @property def is_internal_ref(self): """Return path is internal ref""" return self._is_internal_ref def __repr__(self): return self._path class Link(File): """A link to a given path. Which can be used inside steps.""" def create_action(self, path, name, new_file_path): """Create link to file in work_dir""" # Manipulate target_path if a new relative name path was selected if os.path.isabs(path): target_path = path else: target_path = os.path.relpath(path, os.path.dirname(new_file_path)) LOGGER.debug(" link \"{0}\" <- \"{1}\"".format(target_path, name)) if not jube2.conf.DEBUG_MODE and not os.path.exists(new_file_path): os.symlink(target_path, new_file_path) def etree_repr(self): """Return etree object representation""" link_etree = ET.Element("link") link_etree.text = self._path if self._name is not None: link_etree.attrib["name"] = self._name if self._active != "true": link_etree.attrib["active"] = self._active if self._source_dir != "": link_etree.attrib["source_dir"] = self._source_dir if self._target_dir != "": link_etree.attrib["target_dir"] = self._target_dir if self._is_internal_ref: link_etree.attrib["rel_path_ref"] = "internal" if self._file_path_ref != "": link_etree.attrib["file_path_ref"] = self._file_path_ref return link_etree class Copy(File): """A file or directory given by path. Which can be copied to the work_dir inside steps. """ def create_action(self, path, name, new_file_path): """Copy file/directory to work_dir""" LOGGER.debug(" copy \"{0}\" -> \"{1}\"".format(path, name)) if not jube2.conf.DEBUG_MODE and not os.path.exists(new_file_path): if os.path.isdir(path): shutil.copytree(path, new_file_path, symlinks=True) else: shutil.copy2(path, new_file_path) def etree_repr(self): """Return etree object representation""" copy_etree = ET.Element("copy") copy_etree.text = self._path if self._name is not None: copy_etree.attrib["name"] = self._name if self._active != "true": copy_etree.attrib["active"] = self._active if self._source_dir != "": copy_etree.attrib["source_dir"] = self._source_dir if self._target_dir != "": copy_etree.attrib["target_dir"] = self._target_dir if self._is_internal_ref: copy_etree.attrib["rel_path_ref"] = "internal" if self._file_path_ref != "": copy_etree.attrib["file_path_ref"] = self._file_path_ref return copy_etree class Prepare(jube2.step.Operation): """Prepare the workpackage work directory""" def __init__(self, cmd, stdout_filename=None, stderr_filename=None, work_dir=None, active="true"): jube2.step.Operation.__init__(self, do=cmd, stdout_filename=stdout_filename, stderr_filename=stderr_filename, active=active, work_dir=work_dir) def execute(self, parameter_dict, work_dir, only_check_pending=False, environment=None): """Execute the prepare command""" jube2.step.Operation.execute( self, parameter_dict=parameter_dict, work_dir=work_dir, only_check_pending=only_check_pending, environment=environment) def etree_repr(self): """Return etree object representation""" do_etree = ET.Element("prepare") do_etree.text = self._do if self._stdout_filename is not None: do_etree.attrib["stdout"] = self._stdout_filename if self._stderr_filename is not None: do_etree.attrib["stderr"] = self._stderr_filename if self._active != "true": do_etree.attrib["active"] = self._active if self._work_dir is not None: do_etree.attrib["work_dir"] = self._work_dir return do_etree ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/help.py0000644000175000017500000000316400000000000015764 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """User help""" from __future__ import (print_function, unicode_literals, division) import jube2 import os import re HELP = dict() def load_help(): """Load additional documentation out of help file and add these data to global help dictionary.""" path = os.path.join(jube2.__path__[0], "help.txt") help_file = open(path, "r") group = None # skip header lines i = 0 while i < 4: help_file.readline() i += 1 for line in help_file: # search for new abstract inside of help file matcher = re.match(r"^(\S+)s*$", line) if matcher is not None: group = matcher.group(1) HELP[group] = "" else: if (len(line) > 0) and (group is not None): HELP[group] += line[0] + line[3:] help_file.close() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/help.txt0000644000175000017500000010567500000000000016165 0ustar00sierrousierrou00000000000000Glossary ******** 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 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. ... continue Continue an existing benchmark. Not finished steps will be continued, if they are leaving pending mode. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. copy_tag A copy can be used to copy a file or directory from your normal filesytem to your sandbox work directory. ... * "source_dir" is optional, will be used as a prefix for the source filenames * "target_dir" is optional, will be used as a prefix for the target filenames * "name" is optional, it can be used to rename the file inside your work directory (will be ignored if you use shell extensions in your pathname) * "rel_path_ref" is optional * "external" or "internal" can be chosen, default: external * "external": rel.-paths based on position of xml-file * "internal": rel.-paths based on current work directory (e.g. to link files of another step) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * each copy-tag can contain a list of filenames (or directories), separated by ",", the default separator can be changed by using the "separator" attribute * if "name" is present, the lists must have the same length * you can copy all files inside a directory by using "directory/*" * this cannot be mixed using "name" * in the execution step the given files or directories will be copied database_tag Create sqlite3 database ... ... * "name": name of the table in the database * "" must contain an single parameter or pattern name * "primekeys" is optional: can contain a list of parameter or pattern names (separated by ,). Given parameters or patterns will be used as primary keys of the database table. All primekeys have to be listed as a "" as well. Modification of primary keys of an existing table is not supported. If no primekeys are set then each *jube result* will add new rows to the database. Otherwise rows with matching primekeys will be updated. * "file" is optional. The given value should hold the full path to the database file. If the file including the path does not exists it will be created. Absolute and relative paths are supported. * "filter" is optional. It can contain a bool expression to show only specific result entries. directory_structure * every (new) benchmark run will create its own directory structure * every single workpackage will create its own directory structure * user can add files (or links) to the workpackage dir, but the real position in filesystem will be seen as a blackbox * general directory structure: benchmark_runs (given by "outpath" in xml-file) | +- 000000 (determined through benchmark-id) | +- 000000_compile (step: just an example, can be arbitrary chosen) | +- work (user environment) +- done (workpackage finished information file) +- ... (more jube internal information files) +- 000001_execute | +- work | +- compile -> ../../000000_compile/work (automatic generated link for depending step) +- wp_done_00 (single "do" finished, but not the whole workpackage) +- ... +- 000002_execute +- result (result data) +- configuration.xml (benchmark configuration information file) +- workpackages.xml (workpackage graph information file) +- analyse.xml (analyse data) +- 000001 (determined through benchmark-id) | +- 000000_compile (step: just an example, can be arbitrary chosen) +- 000001_execute +- 000002_postprocessing do_tag A do contain a executable *Shell* operation. ... ... ... ... ... * "do" can contain any *Shell*-syntax-snippet (*parameter* will be replaced "... $nameofparameter ...") * "stdout"- and "stderr"-filename are optional (default: "stdout" and "stderr") * "work_dir" is optional, it can be used to change the work directory of this single command (relativly seen towards the original work directory) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * "done_file"-filename and "error_file" are optional * by using "done_file" the user can mark async-steps. The operation will stop until the script will create the named file inside the work directory. * by using "error_file" the operation will produce a error if the named file can be found inside the work directory. This feature can be used together with the "done_file" to signalise broken async-steps. * "break_file"-filename is optional * by using "break_file" the user can stop further cycle runs. the current step will be directly marked with finalized and further "" will be ignored. * "shared="true"" * can be used inside a step using a shared folder * cmd will be **executed inside the shared folder** * cmd will run once (synchronize all workpackages) * "$jube_wp_..." - parameter cannot be used inside the shared command fileset_tag A fileset is a container to store a bundle of links and copy commands. ... ... ... ... * init_with is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a fileset using the given name, all link and copy will be copied to the local set * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * link and copy can be mixed within one fileset (or left) * filesets can be used inside the step-command general_structure_xml ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... general_structure_yaml # optional additional include paths include-path: ... # optional benchmark selection selection: only: ... not: ... # 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_iteration": current iteration number (default: 0) * "$jube_wp_parent__id": workpackage id of selected parent step * "$jube_wp_relpath": relative path to workpackage work directory (relative towards configuration file) * "$jube_wp_abspath": absolute path to workpackage work directory * "$jube_wp_envstr": a string containing all exported parameter in shell syntax: export par=$par export par2=$par2 * "$jube_wp_envlist": list of all exported parameter names * "$jube_wp_cycle": id of current step cycle (starts at 0) key_tag A syslog result key. "" must contain an single parameter- or patternname. ... * "title" is optional: alternative key title * "format" can contain a C like format string: e.g. "format=".2f"" link_tag A link can be used to create a symbolic link from your sandbox work directory to a file or directory inside your normal filesystem. ... * "source_dir" is optional, will be used as a prefix for the source filenames * "target_dir" is optional, will be used as a prefix for the target filenames * "name" is optional, it can be used to rename the file inside your work directory (will be ignored if you use shell extensions in your pathname) * "rel_path_ref" is optional * "external" or "internal" can be chosen, default: external * "external": rel.-paths based on position of xml-file * "internal": rel.-paths based on current work directory (e.g. to link files of another step) * "active" is optional * can be set to "true" or "false" or any *Python* parsable bool expression to enable or disable the single command * *parameter* are allowed inside this attribute * each link-tag can contain a list of filenames (or directories), separated by ",", the default separator can be changed by using the "separator" attribute * if "name" is present, the lists must have the same length * in the execution step the given files or directories will be linked log Show logs for the given benchmark directory or a given benchmark. If no benchmark id is given, last benchmark found in directory will be used. If benchmark directory is missing, current directory will be used. 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 "" * 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 (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 Select benchmarks by name. ... ... ... * 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 ..." substituteset_tag A substituteset is a container to store a bundle of *sub* commands. ... ... * init_with is optional * if the given filepath can be found inside of the "JUBE_INCLUDE_PATH" and if it contains a substituteset using the given name, all iofile and sub will be copied to the local set * local "iofile" will overwrite imported ones based on "out", local "sub" will overwrite imported ones based on "source" * the name of the external set can differ to the local one by using "init-with="filename.xml:external_name"" * substitutesets can be used inside the step-command syslog_tag A syslog result type ... ... * Syslog deamon can be given by a "host" and "port" combination (default "port": 541) or by a socket "address" e.g.: "/dev/log" (mixing of host and address is not allowed) * "format" is optional: can contain a log format written in a pythonic way (default: "jube[%(process)s]: %(message)s") * "sort" is optional: can contain a list of parameter- or patternnames (separated by ,). Given patterntype or parametertype will be used for sorting * "" must contain an single parameter- or patternname * "filter" is optional, it can contain a bool expression to show only specific result entries table_tag A simple ASCII based table ouput. ... ...
* "style" is optional; allowed styles: "csv", "pretty", "aligned"; default: "csv" * "separator" is optional; only used in csv-style, default: "," * "sort" is optional: can contain a list of parameter- or patternnames (separated by ,). Given patterntype or parametertype will be used for sorting * "" must contain an single parameter- or patternname * "transpose" is optional (default: "false") * "filter" is optional, it can contain a bool expression to show only specific result entries tagging Tagging is a simple way to mark parts of your input file to be includable or excludable. * Every available "" (not the root ""-tag) can contain a tag-attribute * The tag-attribute can contain a list of names: "tag="a,b,c"" or "not" names: "tag="a,!b,c"" * When running *JUBE*, multiple tags can be send to the input-file parser: jube run --tag a b * "" which does not contain one of these names will be hidden inside the include file * which does not contain any tag-attribute will stay inside the include file * "not" tags are more important than normal tags: "tag="a,!b,c"" and running with "a b" will hide the "" because the "!b" is more important than the "a" types *Parameter* and *Pattern* allow a type specification. This type is either used for sorting within the result table and is also used to validate the parameter content. The types are not used to convert parameter values, e.g. a floating value will stay unchanged when used in any other context even if the type int was specified. allowed types are: * "string" (this is also the default type) * "int" * "float" update Check if a newer JUBE version is available. update_mode The update mode is parameter attribute which can be used to control the reevaluation of the parameter content. These update modes are available: * "never": no reevaluation, even if the parameterset is used multiple times * "use": reevaluation if the parameterset is explicitly used * "step": reevaluation in each new step * "cycle": reevaluation in each cycle (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! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/info.py0000644000175000017500000003101500000000000015763 0ustar00sierrousierrou00000000000000## JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Gives benchmark related info""" from __future__ import (print_function, unicode_literals, division) import jube2.util.util import jube2.util.output import jube2.conf import jube2.jubeio import os import time import textwrap import operator def print_benchmarks_info(path): """Print list of all benchmarks, found in given directory""" # Get list of all files and directories in given path if not os.path.isdir(path): raise OSError("Not a directory: \"{0}\"".format(path)) dir_list = os.listdir(path) benchmark_info = list() # Search for possible benchmark dirs for dir_name in dir_list: dir_path = os.path.join(path, dir_name) configuration_file = \ os.path.join(dir_path, jube2.conf.CONFIGURATION_FILENAME) if os.path.isdir(dir_path) and os.path.exists(configuration_file): try: id_number = int(dir_name) parser = jube2.jubeio.Parser(configuration_file) name_str, comment_str, tags = parser.benchmark_info_from_xml() tags_str = jube2.conf.DEFAULT_SEPARATOR.join(tags) # Read timestamps from timestamps file timestamps = \ jube2.util.util.read_timestamps( os.path.join(dir_path, jube2.conf.TIMESTAMPS_INFO)) if "start" in timestamps: time_start = timestamps["start"] else: time_start = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(configuration_file))) if "change" in timestamps: time_change = timestamps["change"] else: time_change = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getmtime(dir_path))) benchmark_info.append([id_number, name_str, time_start, time_change, comment_str, tags_str]) except ValueError: pass # sort using id benchmark_info = sorted(benchmark_info, key=operator.itemgetter(0)) # convert id to string for info in benchmark_info: info[0] = str(info[0]) # add header benchmark_info = [("id", "name", "started", "last change", "comment", "tags")] + benchmark_info if len(benchmark_info) > 1: infostr = (jube2.util.output.text_boxed("Benchmarks found in \"{0}\":". format(path)) + "\n" + jube2.util.output.text_table(benchmark_info, use_header_line=True)) print(infostr) else: print("No Benchmarks found in \"{0}\"".format(path)) def print_benchmark_info(benchmark): """Print information concerning a single benchmark""" infostr = \ jube2.util.output.text_boxed("{0} id:{1} tags:{2}\n\n{3}" .format(benchmark.name, benchmark.id, jube2.conf.DEFAULT_SEPARATOR.join( benchmark.tags), benchmark.comment)) print(infostr) continue_possible = False print(" Directory: {0}" .format(os.path.abspath(benchmark.bench_dir))) # Read timestamps from timestamps file timestamps = jube2.util.util.read_timestamps( os.path.join(benchmark.bench_dir, jube2.conf.TIMESTAMPS_INFO)) if "start" in timestamps: time_start = timestamps["start"] else: # Starttime is workpackage.xml creation time time_start = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(os.path.join( benchmark.bench_dir, jube2.conf.CONFIGURATION_FILENAME)))) if "change" in timestamps: time_change = timestamps["change"] else: time_change = time.strftime( "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getmtime(benchmark.bench_dir))) print("\n Started: {0}".format(time_start)) print("Last change: {0}".format(time_change)) # Create step overview step_info = [("step name", "depends", "#work", "#error", "#done", "last finished")] for step_name, workpackages in benchmark.workpackages.items(): cnt_done = 0 cnt_error = 0 last_finish = time.localtime(0) depends = jube2.conf.DEFAULT_SEPARATOR.join( benchmark.steps[step_name].depend) for workpackage in workpackages: if workpackage.done: cnt_done += 1 # Read timestamp from done_file if it is available otherwise # use mtime done_file = os.path.join(workpackage.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) done_file_f = open(done_file, "r") done_str = done_file_f.read().strip() done_file_f.close() try: done_time = time.strptime(done_str, "%Y-%m-%d %H:%M:%S") except ValueError: done_time = time.localtime(os.path.getmtime(done_file)) last_finish = max(last_finish, done_time) if workpackage.error: cnt_error += 1 if last_finish > time.localtime(0): last_finish_str = time.strftime("%Y-%m-%d %H:%M:%S", last_finish) else: last_finish_str = "" continue_possible = continue_possible or \ (len(workpackages) != cnt_done) # Create #workpackages string iterations = benchmark.steps[step_name].iterations if benchmark.steps[step_name].iterations > 1: cnt = "{0}*{1}".format(len(workpackages) // iterations, iterations) else: cnt = str(len(workpackages)) step_info.append((step_name, depends, cnt, str(cnt_error), str(cnt_done), last_finish_str)) print( "\n" + jube2.util.output.text_table(step_info, use_header_line=True, indent=1)) if continue_possible: print("\n--- Benchmark not finished! ---\n") else: print("\n--- Benchmark finished ---\n") print(jube2.util.output.text_line()) def print_step_info(benchmark, step_name, parametrization_only=False, parametrization_only_csv=False): """Print information concerning a single step in a specific benchmark""" if step_name not in benchmark.workpackages: print("Step \"{0}\" not found in benchmark \"{1}\"." .format(step_name, benchmark.name)) return if parametrization_only_csv: parametrization_only = True if not parametrization_only: print(jube2.util.output.text_boxed( "{0} Step: {1}".format(benchmark.name, step_name))) step = benchmark.steps[step_name] # Get all possible error filenames error_file_names = set() for operation in step.operations: if operation.stderr_filename is not None: error_file_names.add(operation.stderr_filename) else: error_file_names.add("stderr") wp_info = [("id", "started?", "error?", "done?", "work_dir")] error_dict = dict() parameter_list = list() useable_parameter = None for workpackage in benchmark.workpackages[step_name]: # Parameter substitution to use alt_work_dir parameter = \ dict([[par.name, par.value] for par in workpackage.parameterset.constant_parameter_dict.values()]) # Save available parameter names if useable_parameter is None: useable_parameter = [name for name in parameter.keys()] useable_parameter.sort() id_str = str(workpackage.id) started_str = str(workpackage.started).lower() error_str = str(workpackage.error).lower() done_str = str(workpackage.done).lower() work_dir = workpackage.work_dir if step.alt_work_dir is not None: work_dir = jube2.util.util.substitution(step.alt_work_dir, parameter) # collect parameterization parameter_list.append(dict()) parameter_list[-1]["id"] = str(workpackage.id) for parameter in workpackage.parameterset: parameter_list[-1][parameter.name] = parameter.value # Read error-files for error_file_name in error_file_names: if os.path.exists(os.path.join(work_dir, error_file_name)): error_file = open(os.path.join(work_dir, error_file_name), "r") error_string = error_file.read().strip() if len(error_string) > 0: error_dict[os.path.abspath(os.path.join( work_dir, error_file_name))] = error_string error_file.close() # Store info data wp_info.append( (id_str, started_str, error_str, done_str, os.path.abspath(work_dir))) if not parametrization_only: print("Workpackages:") print(jube2.util.output.text_table(wp_info, use_header_line=True, indent=1, auto_linebreak=False)) if (useable_parameter is not None) and (not parametrization_only): print("Available parameter:") wraps = textwrap.wrap(", ".join(useable_parameter), 80) for wrap in wraps: print(wrap) print("") if not parametrization_only: print("Parameterization:") for parameter_dict in parameter_list: print(" ID: {0}".format(parameter_dict["id"])) for name, value in parameter_dict.items(): if name != "id": print(" {0}: {1}".format(name, value)) print("") else: # Create parameterization table table_data = list() table_data.append(list()) table_data[0].append("id") if len(parameter_list) > 0: for name in parameter_list[0]: if name != "id": table_data[0].append(name) for parameter_dict in parameter_list: table_data.append(list()) for name in table_data[0]: table_data[-1].append(parameter_dict[name]) print(jube2.util.output.text_table( table_data, use_header_line=True, indent=1, align_right=True, auto_linebreak=False, style="csv" if parametrization_only_csv else "pretty", separator=(parametrization_only_csv if (parametrization_only_csv) else None))) if not parametrization_only: if len(error_dict) > 0: print("!!! Errors found !!!:") for error_file in error_dict: print(">>> {0}:".format(error_file)) try: print("{0}\n".format(error_dict[error_file])) except UnicodeDecodeError: print("\n") def print_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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/jubeio.py0000644000175000017500000022177300000000000016321 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Basic I/O module""" from __future__ import (print_function, unicode_literals, division) import xml.etree.ElementTree as ET import os from jube2.util.util import Queue import jube2.benchmark import jube2.substitute import jube2.parameter import jube2.fileset import jube2.pattern import jube2.workpackage import jube2.analyser import jube2.step import jube2.util.util import jube2.util.output import jube2.conf import jube2.result_types.syslog import jube2.result_types.table import jube2.result_types.database import jube2.util.yaml_converter import sys import re import copy import hashlib import jube2.log from distutils.version import StrictVersion LOGGER = jube2.log.get_logger(__name__) class Parser(object): """JUBE XML input file parser""" def __init__(self, filename, tags=None, include_path=None, force=False, strict=False): self._filename = filename if include_path is None: include_path = list() self._include_path = include_path if tags is None: tags = set() self._tags = tags self._force = force self._strict = strict self._file_handle = None def __del__(self): if self._file_handle is not None: self._file_handle.close() @property def file_path_ref(self): """Return file path given by config file""" file_path_ref = os.path.dirname(self._filename) if len(file_path_ref) > 0: return file_path_ref else: return "." def benchmarks_from_xml(self): """Return a dict of benchmarks Here parametersets are global and accessible to all benchmarks defined in the corresponding XML file. """ benchmarks = dict() LOGGER.debug("Parsing {0}".format(self._filename)) if not os.path.isfile(self._filename): raise IOError("Benchmark configuration file not found: \"{0}\"" .format(self._filename)) tree = self._tree_from_file(self._filename) # Check compatible terminal encoding: In some cases, the terminal env. # only allow ascii based encoding, print and filesystem operation will # be broken if there is a special char inside the input file. # In such cases the encode will stop, using an UnicodeEncodeError try: xml = jube2.util.output.element_tree_tostring(tree.getroot(), encoding="UTF-8") xml.encode(sys.getfilesystemencoding()) except UnicodeEncodeError as uee: raise ValueError("Your terminal only allows '{0}' encoding. {1}" .format(sys.getfilesystemencoding(), str(uee))) # Check input file version version = tree.getroot().get("version") if (version is not None) and (not self._force): version = version.strip() if StrictVersion(version) > StrictVersion(jube2.conf.JUBE_VERSION): if self._strict: error_str = ("Benchmark file \"{0}\" was created using " + "a newer version of JUBE ({1}).\nCurrent " + "JUBE version ({2}) might not be compatible" + ". Due to strict mode, further execution " + "was stopped.").format( self._filename, version, jube2.conf.JUBE_VERSION) raise ValueError(error_str) else: info_str = ("Benchmark file \"{0}\" was created using a " + "newer version of JUBE ({1}).\nCurrent JUBE " + "version ({2}) might not be compatible." + "\nContinue? (y/n):").format( self._filename, version, jube2.conf.JUBE_VERSION) try: inp = raw_input(info_str) except NameError: inp = input(info_str) if not inp.startswith("y"): return None, list(), list() valid_tags = ["selection", "include-path", "parameterset", "benchmark", "substituteset", "fileset", "include", "patternset"] # Save init include path (from command line) init_include_path = list(self._include_path) # Preprocess xml-tree, this must be done multiple times because of # recursive include structures changed = True counter = 0 while changed and counter < jube2.conf.PREPROCESS_MAX_ITERATION: # Reset variables only_bench = set() not_bench = set() local_tree = copy.deepcopy(tree) self._include_path = list(init_include_path) counter += 1 LOGGER.debug(" --> Preprocess run {0} <--".format(counter)) LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join( self._tags))) Parser._remove_invalid_tags(local_tree.getroot(), self._tags) # Read selection area for selection_tree in local_tree.findall("selection"): new_only_bench, new_not_bench, new_tags = \ Parser._extract_selection(selection_tree) self._tags.update(new_tags) only_bench.update(new_only_bench) not_bench.update(new_not_bench) LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join( self._tags))) # Reset tree, because selection might add additional tags local_tree = copy.deepcopy(tree) Parser._remove_invalid_tags(local_tree.getroot(), self._tags) # Read include-path for include_path_tree in local_tree.findall("include-path"): self._extract_include_path(include_path_tree) # Add env var based include path self._include_path += Parser._read_envvar_include_path() # Add local dir to include path self._include_path += [self.file_path_ref] # Preprocess xml-tree LOGGER.debug(" Preprocess xml tree") for path in self._include_path: LOGGER.debug(" path: {0}".format(path)) changed = self._preprocessor(tree.getroot()) if changed: LOGGER.debug(" New tags might be included, start " + "additional include-preprocess run.") else: LOGGER.debug(" No preprocessing changes were detected, stop" + " additional include-preprocess runs.") # Rerun removing invalid tags LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join(self._tags))) Parser._remove_invalid_tags(tree.getroot(), self._tags) # Check tags for element in tree.getroot(): Parser._check_tag(element, valid_tags) # Check for remaing tags node = jube2.util.util.get_tree_element(tree.getroot(), tag_path="include") if node is not None: raise ValueError(("Remaining include element found, which " + "was not replaced (e.g. due to a missing " + "include-path):\n" + "") .format(node.attrib["from"])) LOGGER.debug(" Preprocess done") # Read all global parametersets global_parametersets = self._extract_parametersets(tree) # Read all global substitutesets global_substitutesets = self._extract_substitutesets(tree) # Read all global filesets global_filesets = self._extract_filesets(tree) # Read all global patternsets global_patternsets = self._extract_patternsets(tree) # At this stage we iterate over benchmarks benchmark_list = tree.findall("benchmark") for benchmark_tree in benchmark_list: self._benchmark_preprocessor(benchmark_tree) benchmark = self._create_benchmark(benchmark_tree, global_parametersets, global_substitutesets, global_filesets, global_patternsets) benchmarks[benchmark.name] = benchmark return benchmarks, list(only_bench), list(not_bench) @staticmethod def _convert_old_tag_format(input_string): """Converts the old ,-based tag format into the new tag format""" tags = set(map(lambda x: x.strip(), input_string.split(","))) not_tags = set([tag for tag in tags if tag[0] == "!"]) tags = tags.difference(not_tags) output_string = "+".join(not_tags) if len(output_string) > 0 and len(tags) > 0: output_string += "+" if len(tags) > 0: output_string += "(" + "|".join(tags) + ")" return output_string @staticmethod def _check_valid_tags(element, tags): """Check if element contains only valid tags""" return jube2.util.util.valid_tags(element.get("tag"), tags) @staticmethod def _remove_invalid_tags(etree, tags): """Remove tags which contain an invalid tags-attribute""" children = list(etree) for child in children: if not Parser._check_valid_tags(child, tags): etree.remove(child) continue Parser._remove_invalid_tags(child, tags) def _preprocessor(self, etree): """Preprocess the xml-file by replacing include-tags""" children = list(etree) new_children = list() include_index = 0 changed = False for child in children: # Replace include tags if ((child.tag == "include") and Parser._check_valid_tags(child, self._tags)): filename = Parser._attribute_from_element(child, "from") path = child.get("path", ".") if path == "": path = "." try: file_path = self._find_include_file(filename) include_tree = ET.parse(file_path) # Find external nodes includes = include_tree.findall(path) except ValueError: includes = list() except ET.ParseError: LOGGER.error("Error while parsing {0}:".format(file_path)) raise if len(includes) > 0: # Remove include-node etree.remove(child) # Insert external nodes for include in includes: etree.insert(include_index, include) include_index += 1 new_children.append(include) include_index -= 1 changed = True else: new_children.append(child) include_index += 1 for child in new_children: changed = self._preprocessor(child) or changed return changed def _benchmark_preprocessor(self, benchmark_etree): """Preprocess the xml-tree of given benchmark.""" LOGGER.debug(" Preprocess benchmark xml tree") # Search for and load external set uses = jube2.util.util.get_tree_elements(benchmark_etree, "use") files = dict() for use in uses: from_str = use.get("from", "").strip() if (use.text is not None) and (use.text.strip() != "") and \ (from_str != ""): hash_val = hashlib.md5(from_str.encode()).hexdigest() if hash_val not in files: files[hash_val] = set() set_names = [element.strip() for element in use.text.split(jube2.conf.DEFAULT_SEPARATOR)] for file_str in from_str.split(jube2.conf.DEFAULT_SEPARATOR): parts = file_str.strip().split(":") filename = parts[0].strip() if filename == "": filename = self._filename alt_set_names = set([element.strip() for element in parts[1:]]) if len(alt_set_names) == 0: alt_set_names = set(set_names) for name in alt_set_names: files[hash_val].add((filename, name)) # Replace set-name with an internal one new_use_str = "" for name in set_names: if len(new_use_str) > 0: new_use_str += jube2.conf.DEFAULT_SEPARATOR new_use_str += "jube_{0}_{1}".format(hash_val, name) use.text = new_use_str # Create new xml elements for fileid in files: for filename, name in files[fileid]: set_type = self._find_set_type(filename, name) set_etree = ET.SubElement(benchmark_etree, set_type) set_etree.attrib["name"] = "jube_{0}_{1}".format(fileid, name) set_etree.attrib["init_with"] = "{0}:{1}".format( filename, name) LOGGER.debug(" Created new <{0}>: jube_{1}_{2}".format( set_type, fileid, name)) def _find_include_file(self, filename): """Search for filename in include-pathes and return resulting path""" for path in self._include_path: file_path = os.path.join(path, filename) if os.path.exists(file_path): break else: raise ValueError(("\"{0}\" not found in possible " + "include pathes").format(filename)) return file_path def _find_set_type(self, filename, name): """Search for the set-type inside given file""" LOGGER.debug( " Searching for type of \"{0}\" in {1}".format(name, filename)) file_path = self._find_include_file(filename) etree = self._tree_from_file(file_path).getroot() Parser._remove_invalid_tags(etree, self._tags) found_set = jube2.util.util.get_tree_elements( etree, attribute_dict={"name": name}) found_set = [set_etree for set_etree in found_set if set_etree.tag in ("parameterset", "substituteset", "fileset", "patternset")] if len(found_set) > 1: raise ValueError(("name=\"{0}\" can be found multiple times " + "inside \"{1}\"").format(name, file_path)) elif len(found_set) == 0: raise ValueError(("name=\"{0}\" not found inside " + "\"{1}\"").format(name, file_path)) else: return found_set[0].tag def benchmark_info_from_xml(self): """Return name, comment and available tags of first benchmark found in file""" tree = ET.parse(self._filename).getroot() tags = set() for tag_etree in jube2.util.util.get_tree_elements(tree, "selection/tag"): if tag_etree.text is not None: tags.update(set([tag.strip() for tag in tag_etree.text.split( jube2.conf.DEFAULT_SEPARATOR)])) benchmark_etree = jube2.util.util.get_tree_element(tree, "benchmark") if benchmark_etree is None: raise ValueError("benchmark-tag not found in \"{0}\"".format( self._filename)) name = Parser._attribute_from_element(benchmark_etree, "name").strip() comment_element = benchmark_etree.find("comment") if comment_element is not None: comment = comment_element.text if comment is None: comment = "" else: comment = "" comment = re.sub(r"\s+", " ", comment).strip() return name, comment, tags def analyse_result_from_xml(self): """Read existing analyse out of xml-file""" LOGGER.debug("Parsing {0}".format(self._filename)) try: tree = ET.parse(self._filename).getroot() except ET.ParseError as pe: LOGGER.error( "Parsing error while reading existing analysis: " + "{0}".format(pe)) return None analyse_result = dict() analyser = jube2.util.util.get_tree_elements(tree, "analyzer") analyser += jube2.util.util.get_tree_elements(tree, "analyser") for analyser_etree in analyser: analyser_name = Parser._attribute_from_element( analyser_etree, "name") analyse_result[analyser_name] = dict() for step_etree in analyser_etree: Parser._check_tag(step_etree, ["step"]) step_name = Parser._attribute_from_element( step_etree, "name") analyse_result[analyser_name][step_name] = dict() for workpackage_etree in step_etree: Parser._check_tag(workpackage_etree, ["workpackage"]) wp_id = int(Parser._attribute_from_element( workpackage_etree, "id")) analyse_result[analyser_name][step_name][wp_id] = dict() for pattern_etree in workpackage_etree: Parser._check_tag(pattern_etree, ["pattern"]) pattern_name = \ Parser._attribute_from_element( pattern_etree, "name") pattern_type = \ Parser._attribute_from_element( pattern_etree, "type") value = pattern_etree.text if value is not None: value = value.strip() else: value = "" value = jube2.util.util.convert_type(pattern_type, value) analyse_result[analyser_name][step_name][ wp_id][pattern_name] = value return analyse_result def workpackages_from_xml(self, benchmark): """Read existing workpackage data out of a xml-file""" workpackages = dict() # tmp: Dict workpackage_id => workpackage tmp = dict() # parents_tmp: Dict workpackage_id => list of parent_workpackage_ids parents_tmp = dict() iteration_siblings_tmp = dict() work_list = Queue() LOGGER.debug("Parsing {0}".format(self._filename)) if not os.path.isfile(self._filename): raise IOError("Workpackage configuration file not found: \"{0}\"" .format(self._filename)) tree = ET.parse(self._filename) max_id = -1 for element in tree.getroot(): Parser._check_tag(element, ["workpackage"]) # Read XML-data (workpackage_id, step_name, parameterset, parents, iteration_siblings, iteration, cycle, set_env, unset_env) = \ Parser._extract_workpackage_data(element) # Search for step step = benchmark.steps[step_name] parameter_names = [parameter.name for parameter in parameterset] tmp[workpackage_id] = \ jube2.workpackage.Workpackage(benchmark, step, parameter_names, parameterset, workpackage_id, iteration, cycle) max_id = max(max_id, workpackage_id) parents_tmp[workpackage_id] = parents iteration_siblings_tmp[workpackage_id] = iteration_siblings tmp[workpackage_id].env.update(set_env) for env_name in unset_env: if env_name in tmp[workpackage_id].env: del tmp[workpackage_id].env[env_name] if len(parents) == 0: work_list.put(tmp[workpackage_id]) # Set workpackage counter to current id number jube2.workpackage.Workpackage.id_counter = max_id + 1 # Rebuild graph structure for workpackage_id in parents_tmp: for parent_id in parents_tmp[workpackage_id]: tmp[workpackage_id].add_parent(tmp[parent_id]) tmp[parent_id].add_children(tmp[workpackage_id]) # Rebuild sibling structure for workpackage_id in iteration_siblings_tmp: for sibling_id in iteration_siblings_tmp[workpackage_id]: tmp[workpackage_id].iteration_siblings.add(tmp[sibling_id]) # Rebuild history done_list = list() while not work_list.empty(): workpackage = work_list.get_nowait() history = jube2.parameter.Parameterset() if workpackage.id in parents_tmp: for parent_id in parents_tmp[workpackage.id]: history.add_parameterset(tmp[parent_id].parameterset) done_list.append(workpackage) for child in workpackage.children: all_done = True for parent in child.parents: all_done = all_done and (parent in done_list) if all_done and (child not in done_list): work_list.put(child) history.add_parameterset(workpackage.parameterset) workpackage.parameterset.add_parameterset(history) # Add JUBE parameter for workpackage in tmp.values(): # JUBE benchmark parameter workpackage.parameterset.add_parameterset( benchmark.get_jube_parameterset()) # JUBE step parameter workpackage.parameterset.add_parameterset( workpackage.step.get_jube_parameterset()) # JUBE workpackage parameter workpackage.parameterset.add_parameterset( workpackage.get_jube_parameterset()) # Enable work_dir caching workpackage.allow_workpackage_dir_caching() jube_parameter = workpackage.parameterset.get_updatable_parameter( jube2.parameter.JUBE_MODE) jube_parameter.parameter_substitution( additional_parametersets=[workpackage.parameterset], final_sub=True) workpackage.parameterset.update_parameterset(jube_parameter) # Store workpackage data work_stat = jube2.util.util.WorkStat() for step_name in benchmark.steps: workpackages[step_name] = list() # First put started wps inside the queue for mode in ("only_started", "all"): for workpackage in tmp.values(): if len(workpackage.parents) == 0: if (mode == "only_started" and workpackage.started) or \ (mode == "all" and (not workpackage.queued)): workpackage.queued = True work_stat.put(workpackage) if mode == "all": workpackages[workpackage.step.name].append(workpackage) return workpackages, work_stat @staticmethod def _extract_workpackage_data(workpackage_etree): """Extract workpackage information from etree Return workpackage id, name of step, local parameterset and list of parent ids """ valid_tags = ["step", "parameterset", "parents", "iteration_siblings", "environment"] for element in workpackage_etree: Parser._check_tag(element, valid_tags) workpackage_id = int(Parser._attribute_from_element( workpackage_etree, "id")) step_etree = workpackage_etree.find("step") iteration = int(step_etree.get("iteration", "0").strip()) cycle = int(step_etree.get("cycle", "0").strip()) step_name = step_etree.text.strip() parameterset_etree = workpackage_etree.find("parameterset") if parameterset_etree is not None: parameters = Parser._extract_parameters(parameterset_etree) else: parameters = list() parameterset = jube2.parameter.Parameterset() for parameter in parameters: parameterset.add_parameter(parameter) parents_etree = workpackage_etree.find("parents") if parents_etree is not None: parents = [int(parent) for parent in parents_etree.text.split(",")] else: parents = list() siblings_etree = workpackage_etree.find("iteration_siblings") if siblings_etree is not None: iteration_siblings = set([int(sibling) for sibling in siblings_etree.text.split(",")]) else: iteration_siblings = set([workpackage_id]) environment_etree = workpackage_etree.find("environment") set_env = dict() unset_env = list() if environment_etree is not None: for env_etree in environment_etree: env_name = Parser._attribute_from_element(env_etree, "name") if env_etree.tag == "env": if env_etree.text is not None: set_env[env_name] = env_etree.text.strip() # string repr must be evaluated if (set_env[env_name][0] == "'") or \ ((set_env[env_name][0] == "u") and (set_env[env_name][1] == "'")) and \ (set_env[env_name][-1] == "'"): set_env[env_name] = eval(set_env[env_name]) elif env_etree.tag == "nonenv": unset_env.append(env_name) return (workpackage_id, step_name, parameterset, parents, iteration_siblings, iteration, cycle, set_env, unset_env) @staticmethod def _extract_selection(selection_etree): """Extract selction information from etree Return names of benchmarks and tags (set([only,...]),set([not,...]), set([tag, ...])) """ LOGGER.debug(" Parsing ") valid_tags = ["only", "not", "tag"] only_bench = list() not_bench = list() tags = set() for element in selection_etree: Parser._check_tag(element, valid_tags) separator = jube2.conf.DEFAULT_SEPARATOR if element.text is not None: if element.tag == "only": only_bench += element.text.split(separator) elif element.tag == "not": not_bench += element.text.split(separator) elif element.tag == "tag": tags.update(set([tag.strip() for tag in element.text.split(separator)])) only_bench = set([bench.strip() for bench in only_bench]) not_bench = set([bench.strip() for bench in not_bench]) return only_bench, not_bench, tags def _extract_include_path(self, include_path_etree): """Extract include-path pathes from etree""" LOGGER.debug(" Parsing ") valid_tags = ["path"] pathes = [] if len(include_path_etree.text.strip()) > 0: pathes.append(include_path_etree.text.strip()) for element in include_path_etree: Parser._check_tag(element, valid_tags) path = element.text if path is None: raise ValueError("Empty \"\" found") path = path.strip() if len(path) == 0: raise ValueError("Empty \"\" found") pathes.append(path) for path in pathes: path = os.path.expandvars(os.path.expanduser(path)) path = os.path.join(self.file_path_ref, path) self._include_path += [path] LOGGER.debug(" New path: {0}".format(path)) @staticmethod def _read_envvar_include_path(): """Add environment var include-path""" LOGGER.debug(" Read $JUBE_INCLUDE_PATH") if "JUBE_INCLUDE_PATH" in os.environ: return [include_path for include_path in os.environ["JUBE_INCLUDE_PATH"].split(":") if include_path != ""] else: return [] def _create_benchmark(self, benchmark_etree, global_parametersets, global_substitutesets, global_filesets, global_patternsets): """Create benchmark from etree Return a benchmark """ name = \ Parser._attribute_from_element(benchmark_etree, "name").strip() valid_tags = ["parameterset", "substituteset", "fileset", "step", "comment", "patternset", "analyzer", "analyser", "result"] for element in benchmark_etree: Parser._check_tag(element, valid_tags) comment_element = benchmark_etree.find("comment") if comment_element is not None: comment = comment_element.text if comment is None: comment = "" else: comment = "" comment = re.sub(r"\s+", " ", comment).strip() outpath = Parser._attribute_from_element(benchmark_etree, "outpath").strip() outpath = os.path.expandvars(os.path.expanduser(outpath)) # Add position of user to outpath outpath = os.path.normpath(os.path.join(self.file_path_ref, outpath)) file_path_ref = benchmark_etree.get("file_path_ref") # Combine global and local sets parametersets = \ Parser._combine_global_and_local_sets( global_parametersets, self._extract_parametersets(benchmark_etree)) substitutesets = \ Parser._combine_global_and_local_sets( global_substitutesets, self._extract_substitutesets(benchmark_etree)) filesets = \ Parser._combine_global_and_local_sets( global_filesets, self._extract_filesets(benchmark_etree)) patternsets = \ Parser._combine_global_and_local_sets( global_patternsets, self._extract_patternsets(benchmark_etree)) # dict of local steps steps = self._extract_steps(benchmark_etree) # dict of local analysers analyser = self._extract_analysers(benchmark_etree) # dict of local results results, results_order = self._extract_results(benchmark_etree) # File path reference for relative file location if file_path_ref is not None: file_path_ref = file_path_ref.strip() file_path_ref = \ os.path.expandvars(os.path.expanduser(file_path_ref)) else: file_path_ref = "." # Add position of user to file_path_ref file_path_ref = \ os.path.normpath(os.path.join(self.file_path_ref, file_path_ref)) benchmark = jube2.benchmark.Benchmark(name, outpath, parametersets, substitutesets, filesets, patternsets, steps, analyser, results, results_order, comment, self._tags, file_path_ref) return benchmark @staticmethod def _combine_global_and_local_sets(global_sets, local_sets): """Combine global and local sets """ result_sets = dict(global_sets) if set(result_sets) & set(local_sets): raise ValueError("\"{0}\" not unique" .format(",".join([name for name in (set(result_sets) & set(local_sets))]))) result_sets.update(local_sets) return result_sets @staticmethod def _extract_steps(etree): """Extract all steps from benchmark Return a dict of steps, e.g. {"compile": Step(...), ...} """ steps = dict() for element in etree.findall("step"): step = Parser._extract_step(element) if step.name in steps: raise ValueError("\"{0}\" not unique".format(step.name)) steps[step.name] = step return steps @staticmethod def _extract_step(etree_step): """Extract a step from etree Return name, list of contents (dicts), depend (list of strings). """ valid_tags = ["use", "do"] name = Parser._attribute_from_element(etree_step, "name").strip() LOGGER.debug(" Parsing ".format(name)) tmp = etree_step.get("depend", "").strip() iterations = int(etree_step.get("iterations", "1").strip()) alt_work_dir = etree_step.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() export = etree_step.get("export", "false").strip().lower() == "true" max_wps = etree_step.get("max_async", "0").strip() active = etree_step.get("active", "true").strip() suffix = etree_step.get("suffix", "").strip() cycles = int(etree_step.get("cycles", "1").strip()) procs = int(etree_step.get("procs", "1").strip()) do_log_file = etree_step.get("do_log_file", "None").strip() do_log_file = None if do_log_file == "None" else do_log_file do_log_file = None if do_log_file == "False" else do_log_file do_log_file = None if do_log_file == "false" else do_log_file do_log_file = jube2.conf.DO_LOG_FILENAME if do_log_file == "True" else do_log_file do_log_file = jube2.conf.DO_LOG_FILENAME if do_log_file == "true" else do_log_file shared_name = etree_step.get("shared") if shared_name is not None: shared_name = shared_name.strip() if shared_name == "": raise ValueError("Empty \"shared\" attribute in " + " found.") depend = set(val.strip() for val in tmp.split(jube2.conf.DEFAULT_SEPARATOR) if val.strip()) step = jube2.step.Step(name, depend, iterations, alt_work_dir, shared_name, export, max_wps, active, suffix, cycles, procs, do_log_file) for element in etree_step: Parser._check_tag(element, valid_tags) if element.tag == "do": async_filename = element.get("done_file") if async_filename is not None: async_filename = async_filename.strip() error_filename = element.get("error_file") if error_filename is not None: error_filename = error_filename.strip() break_filename = element.get("break_file") if break_filename is not None: break_filename = break_filename.strip() stdout_filename = element.get("stdout") if stdout_filename is not None: stdout_filename = stdout_filename.strip() stderr_filename = element.get("stderr") if stderr_filename is not None: stderr_filename = stderr_filename.strip() active = element.get("active", "true").strip() shared_str = element.get("shared", "false").strip() alt_work_dir = element.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() if shared_str.lower() == "true": if shared_name is None: raise ValueError(" only allowed " "inside a which has a shared " "region") if procs != 1: raise ValueError(" not allowed " + "inside a parallel ") shared = True elif shared_str == "false": shared = False else: raise ValueError("shared=\"{0}\" not allowed. Must be " + "\"true\" or \"false\"".format( shared_str)) cmd = element.text if cmd is None: cmd = "" operation = jube2.step.Operation(cmd.strip(), async_filename, stdout_filename, stderr_filename, active, shared, alt_work_dir, break_filename, error_filename) step.add_operation(operation) elif element.tag == "use": step.add_uses(Parser._extract_use(element)) return step @staticmethod def _extract_analysers(etree): """Extract all analyser from etree""" analysers = dict() analyser_tags = etree.findall("analyzer") analyser_tags += etree.findall("analyser") for element in analyser_tags: analyser = Parser._extract_analyser(element) if analyser.name in analysers: raise ValueError("\"{0}\" not unique".format(analyser.name)) analysers[analyser.name] = analyser return analysers @staticmethod def _extract_analyser(etree_analyser): """Extract an analyser from etree""" valid_tags = ["use", "analyse"] name = Parser._attribute_from_element(etree_analyser, "name").strip() reduce_iteration = \ etree_analyser.get("reduce", "true").strip().lower() == "true" analyser = jube2.analyser.Analyser(name, reduce_iteration) LOGGER.debug(" Parsing ".format(name)) for element in etree_analyser: Parser._check_tag(element, valid_tags) if element.tag == "analyse": step_name = Parser._attribute_from_element(element, "step").strip() # If there are no files, just add a dummy element to the list if len(element) == 0: analyser.add_analyse(step_name, None) for file_etree in element: if (file_etree.text is None) or \ (file_etree.text.strip() == ""): raise ValueError("Empty found") else: use_text = file_etree.get("use") if use_text is not None: use_names = \ [use_name.strip() for use_name in use_text.split(jube2.conf.DEFAULT_SEPARATOR)] else: use_names = list() for filename in file_etree.text.split( jube2.conf.DEFAULT_SEPARATOR): file_obj = jube2.analyser.Analyser.AnalyseFile( filename.strip()) file_obj.add_uses(use_names) analyser.add_analyse(step_name, file_obj) elif element.tag == "use": analyser.add_uses(Parser._extract_use(element)) return analyser @staticmethod def _extract_results(etree): """Extract all results from etree""" results = dict() results_order = list() valid_tags = ["use", "table", "syslog", "database"] for result_etree in etree.findall("result"): result_dir = result_etree.get("result_dir") if result_dir is not None: result_dir = \ os.path.expandvars(os.path.expanduser(result_dir.strip())) sub_results = dict() uses = list() for element in result_etree: Parser._check_tag(element, valid_tags) if element.tag == "use": uses.append(Parser._extract_use(element)) elif element.tag == "table": result = Parser._extract_table(element) result.result_dir = result_dir elif element.tag == "syslog": result = Parser._extract_syslog(element) elif element.tag == "database": result = Parser._extract_database(element) result.result_dir = result_dir if element.tag in ["table", "syslog", "database"]: if result.name in sub_results: raise ValueError( ("Result name \"{0}\" is used " + "multiple times").format(result.name)) sub_results[result.name] = result if result.name not in results_order: results_order.append(result.name) for result in sub_results.values(): for use in uses: result.add_uses(use) if len(set(results.keys()).intersection( set(sub_results.keys()))) > 0: raise ValueError( ("Result name(s) \"{0}\" is/are used " + "multiple times").format( ",".join(set(results.keys()).intersection( set(sub_results.keys()))))) results.update(sub_results) return results, results_order @staticmethod def _extract_table(etree_table): """Extract a table from etree""" name = Parser._attribute_from_element(etree_table, "name").strip() separator = \ etree_table.get("separator", jube2.conf.DEFAULT_SEPARATOR) style = etree_table.get("style", "csv").strip() if style not in ["csv", "pretty", "aligned"]: raise ValueError("Not allowed style-type \"{0}\" " "in ".format(style, name)) sort_names = etree_table.get("sort", "").split( jube2.conf.DEFAULT_SEPARATOR) sort_names = [sort_name.strip() for sort_name in sort_names] sort_names = [ sort_name for sort_name in sort_names if len(sort_name) > 0] transpose = etree_table.get("transpose") if transpose is not None: transpose = transpose.strip().lower() == "true" else: transpose = False res_filter = etree_table.get("filter") if res_filter is not None: res_filter = res_filter.strip() table = jube2.result_types.table.Table(name, style, separator, sort_names, transpose, res_filter) for element in etree_table: Parser._check_tag(element, ["column"]) column_name = element.text if column_name is None: column_name = "" column_name = column_name.strip() if column_name == "": raise ValueError("Empty not allowed") colw = element.get("colw") if colw is not None: colw = int(colw) title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() table.add_column(column_name, colw, format_string, title) return table @staticmethod def _extract_database(etree_database): """Extract a database result infos from etree""" name = Parser._attribute_from_element(etree_database, "name").strip() res_filter = etree_database.get("filter") if res_filter is not None: res_filter = res_filter.strip() primekeys = etree_database.get("primekeys", "") primekeys = primekeys.replace('[', '').replace(']', '').replace( "'", '').split(jube2.conf.DEFAULT_SEPARATOR) primekeys = [primekey.strip() for primekey in primekeys] primekeys = [primekey for primekey in primekeys if len(primekey) > 0] db_file = etree_database.get("file") database = jube2.result_types.database.Database( name, res_filter, primekeys, db_file) for element in etree_database: Parser._check_tag(element, ["key"]) key_name = element.text if key_name is None: key_name = "" key_name = key_name.strip() if key_name == "": raise ValueError("Empty not allowed") title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() database.add_key(key_name, format_string, title) return database @staticmethod def _extract_syslog(etree_syslog): """Extract requires syslog information from etree.""" name = Parser._attribute_from_element(etree_syslog, "name").strip() # see if the host, port combination or address is given syslog_address = etree_syslog.get("address") if syslog_address is not None: syslog_address = \ os.path.expandvars(os.path.expanduser(syslog_address.strip())) syslog_host = etree_syslog.get("host") if syslog_host is not None: syslog_host = syslog_host.strip() syslog_port = etree_syslog.get("port") if syslog_port is not None: syslog_port = int(syslog_port.strip()) syslog_fmt_string = etree_syslog.get("format") if syslog_fmt_string is not None: syslog_fmt_string = syslog_fmt_string.strip() sort_names = etree_syslog.get("sort", "").split( jube2.conf.DEFAULT_SEPARATOR) sort_names = [sort_name.strip() for sort_name in sort_names] sort_names = [ sort_name for sort_name in sort_names if len(sort_name) > 0] res_filter = etree_syslog.get("filter") if res_filter is not None: res_filter = res_filter.strip() syslog_result = jube2.result_types.syslog.SysloggedResult( name, syslog_address, syslog_host, syslog_port, syslog_fmt_string, sort_names, res_filter) for element in etree_syslog: Parser._check_tag(element, ["key"]) key_name = element.text if key_name is None: key_name = "" key_name = key_name.strip() if key_name == "": raise ValueError("Empty not allowed") title = element.get("title") format_string = element.get("format") if format_string is not None: format_string = format_string.strip() syslog_result.add_key(key_name, format_string, title) return syslog_result @staticmethod def _extract_use(etree_use): """Extract a use from etree""" if etree_use.text is not None: use_names = [use_name.strip() for use_name in etree_use.text.split(jube2.conf.DEFAULT_SEPARATOR)] return use_names else: raise ValueError("Empty found") def _tree_from_file(self, file_path): """Extract a XML tree from a file (doing implicit YAML conversion)""" try: if file_path.endswith(".xml"): return ET.parse(file_path) elif file_path.endswith(".yml") or file_path.endswith(".yaml") or \ jube2.util.yaml_converter.\ YAML_Converter.is_parseable_yaml_file(file_path): include_path = list(self._include_path) include_path += Parser._read_envvar_include_path() file_handle = jube2.util.yaml_converter.YAML_Converter( file_path, include_path, self._tags) data = file_handle.read() tree = ET.ElementTree(ET.fromstring(data)) file_handle.close() return tree else: return ET.parse(file_path) except Exception: LOGGER.error("Error while parsing {0}:".format(file_path)) raise def _extract_extern_set(self, filename, set_type, name, search_name=None, duplicate=None): """Load a parameter-/file-/substitutionset from a given file""" if search_name is None: search_name = name LOGGER.debug(" Searching for <{0} name=\"{1}\"> in {2}" .format(set_type, search_name, filename)) file_path = self._find_include_file(filename) etree = self._tree_from_file(file_path).getroot() Parser._remove_invalid_tags(etree, self._tags) self._preprocessor(etree) result_set = None # Find element in XML-tree elements = jube2.util.util.get_tree_elements(etree, set_type, {"name": search_name}) # Element can also be the root element itself if etree.tag == set_type: element = jube2.util.util.get_tree_element( etree, attribute_dict={"name": search_name}) if element is not None: elements.append(element) test_duplicate=None if duplicate == "###initiated_with_without_duplicate_mentioning###": if elements[0].get("duplicate") != None: duplicate = elements[0].get("duplicate") else: duplicate = "replace" if duplicate != "###initiated_with_without_duplicate_mentioning###" and duplicate != None: if set_type == "parameterset": if elements[0].get("duplicate") == None: test_duplicate = duplicate else: test_duplicate = elements[0].get("duplicate") if duplicate != None: if test_duplicate != duplicate: raise ValueError("The {0} {1} is mentioned at least twice with different duplicate options.".format(set_type, name)) if duplicate == "###initiated_with_without_duplicate_mentioning###": raise Exception("Unknown error in extracting an extern set." + "This should not happen. Please contact the JUBE developers.") if elements is not None: if len(elements) > 1: raise ValueError("\"{0}\" found multiple times in \"{1}\"" .format(search_name, file_path)) elif len(elements) == 0: raise ValueError("\"{0}\" not found in \"{1}\"" .format(search_name, file_path)) init_with = elements[0].get("init_with") # recursive external file open if init_with is not None: parts = init_with.strip().split(":") new_filename = parts[0] if len(parts) > 1: new_search_name = parts[1] else: new_search_name = search_name if (new_filename == filename) and \ (new_search_name == search_name): raise ValueError(("Cannot init <{0} name=\"{1}\"> by " "itself inside \"{2}\"").format( set_type, search_name, file_path)) result_set = self._extract_extern_set(new_filename, set_type, name, new_search_name, duplicate) if set_type == "parameterset": if result_set is None: result_set = jube2.parameter.Parameterset(name, duplicate) for parameter in self._extract_parameters(elements[0]): result_set.add_parameter(parameter) elif set_type == "substituteset": files, subs = self._extract_subs(elements[0]) if result_set is None: result_set = \ jube2.substitute.Substituteset(name, files, subs) else: result_set.update_files(files) result_set.update_substitute(subs) elif set_type == "fileset": if result_set is None: result_set = jube2.fileset.Fileset(name) files = self._extract_files(elements[0]) for file_obj in files: if type(file_obj) is not jube2.fileset.Prepare: file_obj.file_path_ref = \ os.path.join(os.path.dirname(file_path), file_obj.file_path_ref) if not os.path.isabs(file_obj.file_path_ref): file_obj.file_path_ref = \ os.path.relpath(file_obj.file_path_ref, self.file_path_ref) result_set += files elif set_type == "patternset": if result_set is None: result_set = jube2.pattern.Patternset(name) for pattern in self._extract_pattern(elements[0]): result_set.add_pattern(pattern) return result_set else: raise ValueError("\"{0}\" not found in \"{1}\"" .format(name, file_path)) def _extract_parametersets(self, etree): """Return parametersets from etree""" parametersets = dict() for element in etree.findall("parameterset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) duplicate = element.get("duplicate", "replace").strip() if duplicate is None: duplicate="replace" if duplicate != "replace" and duplicate != "concat" and duplicate != "error": raise ValueError("Invalid \"duplicate\" attribute in " + "parameterset {0} found. Use \"replace\" (default)" + ", \"concat\" or \"error\".".format(name)) init_with = element.get("init_with") if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None if element.get("duplicate") == None: duplicate = "###initiated_with_without_duplicate_mentioning###" parameterset = self._extract_extern_set(parts[0], "parameterset", name, search_name, duplicate) else: parameterset = jube2.parameter.Parameterset(name, duplicate) for parameter in self._extract_parameters(element): parameterset.add_parameter(parameter) if parameterset.name in parametersets: raise ValueError( "\"{0}\" not unique".format(parameterset.name)) parametersets[parameterset.name] = parameterset return parametersets @staticmethod def _extract_parameters(etree_parameterset): """Extract parameters from parameterset Return a list of parameters. Parameters might also include lists""" parameters = list() for param in etree_parameterset: Parser._check_tag(param, ["parameter"]) name = Parser._attribute_from_element(param, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") if not re.match(r"^[^\d\W]\w*$", name, re.UNICODE): raise ValueError(("name=\"{0}\" in " + "contains a disallowed " + "character").format(name)) separator = param.get("separator", default=jube2.conf.DEFAULT_SEPARATOR) parameter_type = param.get("type", default="string").strip() parameter_mode = param.get("mode", default="text").strip() parameter_update_mode = param.get("update_mode", default="never").strip() if parameter_update_mode not in jube2.parameter.UPDATE_MODES: raise ValueError( ("update_mode=\"{0}\" in " + " does not exist") .format(parameter_update_mode, name)) export_str = param.get("export", default="false").strip() export = export_str.lower() == "true" duplicate = param.get("duplicate", "none").strip() if duplicate is None: duplicate="none" if duplicate != "replace" and duplicate != "concat" and duplicate != "error" and duplicate != "none": raise ValueError("Invalid \"duplicate\" attribute in " + "parameter {0} found. Use \"replace\"" + ", \"concat\", \"error\" or \"none\" (default).".format(name)) if parameter_mode not in jube2.conf.ALLOWED_MODETYPES: raise ValueError( ("parameter-mode \"{0}\" not allowed in " + "").format(parameter_mode, name)) value_etree = param.find("value") if value_etree is not None: if value_etree.text is None: value = "" else: value = value_etree.text.strip() else: if param.text is None: value = "" else: value = param.text.strip() selection_etree = param.find("selection") if selection_etree is not None: selected_value = selection_etree.text if selected_value is None: selected_value = "" idx = int(selection_etree.get("idx", "-1")) else: selected_value = param.get("selection") idx = -1 if selected_value is not None: selected_value = selected_value.strip() parameter = \ jube2.parameter.Parameter.create_parameter( name, value, separator, parameter_type, selected_value, parameter_mode, export, update_mode=parameter_update_mode, idx=idx, eval_helper=None, fixed=False, duplicate=duplicate) parameters.append(parameter) return parameters def _extract_patternsets(self, etree): """Return patternset from etree""" patternsets = dict() for element in etree.findall("patternset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None patternset = self._extract_extern_set(parts[0], "patternset", name, search_name) else: patternset = jube2.pattern.Patternset(name) for pattern in Parser._extract_pattern(element): patternset.add_pattern(pattern) if patternset.name in patternsets: raise ValueError("\"{0}\" not unique".format(patternset.name)) patternsets[patternset.name] = patternset return patternsets @staticmethod def _extract_pattern(etree_patternset): """Extract pattern from patternset Return a list of pattern""" patternlist = list() for pattern in etree_patternset: Parser._check_tag(pattern, ["pattern"]) name = Parser._attribute_from_element(pattern, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") if not re.match(r"^[^\d\W]\w*$", name, re.UNICODE): raise ValueError(("name=\"{0}\" in " + "contains a disallowed " + "character").format(name)) pattern_mode = pattern.get("mode", default="pattern").strip() if pattern_mode not in \ set(["pattern", "text"]).union( jube2.conf.ALLOWED_SCRIPTTYPES): raise ValueError(("pattern-mdoe \"{0}\" not allowed in " + "").format( pattern_mode, name)) content_type = pattern.get("type", default="string").strip() unit = pattern.get("unit", "").strip() dotall = \ pattern.get("dotall", "false").strip().lower() == "true" default = pattern.get("default") if default is not None: default = default.strip() if pattern.text is None: value = "" else: value = pattern.text.strip() patternlist.append(jube2.pattern.Pattern(name, value, pattern_mode, content_type, unit, default, dotall)) return patternlist def _extract_filesets(self, etree): """Return filesets from etree""" filesets = dict() for element in etree.findall("fileset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError( "Empty \"name\" attribute in found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") filelist = Parser._extract_files(element) if name in filesets: raise ValueError("\"{0}\" not unique".format(name)) if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None filesets[name] = self._extract_extern_set(parts[0], "fileset", name, search_name) else: filesets[name] = jube2.fileset.Fileset(name) filesets[name] += filelist return filesets @staticmethod def _extract_files(etree_fileset): """Return filelist from fileset-etree""" filelist = list() valid_tags = ["copy", "link", "prepare"] for etree_file in etree_fileset: Parser._check_tag(etree_file, valid_tags) if etree_file.tag in ["copy", "link"]: separator = etree_file.get( "separator", jube2.conf.DEFAULT_SEPARATOR) source_dir = etree_file.get("directory", default="").strip() # New source_dir attribute overwrites deprecated directory # attribute source_dir_new = etree_file.get("source_dir") target_dir = etree_file.get("target_dir", default="").strip() if source_dir_new is not None: source_dir = source_dir_new.strip() active = etree_file.get("active", "true").strip() file_path_ref = etree_file.get("file_path_ref") alt_name = etree_file.get("name") # Check if the filepath is relativly seen to working dir or the # position of the xml-input-file is_internal_ref = \ etree_file.get("rel_path_ref", default="external").strip() == "internal" if etree_file.text is None: raise ValueError("Empty filelist in <{0}> found." .format(etree_file.tag)) files = jube2.util.util.safe_split(etree_file.text.strip(), separator) if alt_name is not None: # Use the new alternativ filenames names = [name.strip() for name in alt_name.split(jube2.conf.DEFAULT_SEPARATOR)] if len(names) != len(files): raise ValueError("Namelist and filelist must have " + "same length in <{0}>". format(etree_file.tag)) else: names = None for i, file_path in enumerate(files): path = file_path.strip() if names is not None: name = names[i] else: name = None if etree_file.tag == "copy": file_obj = jube2.fileset.Copy( path, name, is_internal_ref, active, source_dir, target_dir) elif etree_file.tag == "link": file_obj = jube2.fileset.Link( path, name, is_internal_ref, active, source_dir, target_dir) if file_path_ref is not None: file_obj.file_path_ref = \ os.path.expandvars(os.path.expanduser( file_path_ref.strip())) filelist.append(file_obj) elif etree_file.tag == "prepare": cmd = etree_file.text if cmd is None: cmd = "" cmd = cmd.strip() stdout_filename = etree_file.get("stdout") if stdout_filename is not None: stdout_filename = stdout_filename.strip() stderr_filename = etree_file.get("stderr") if stderr_filename is not None: stderr_filename = stderr_filename.strip() alt_work_dir = etree_file.get("work_dir") if alt_work_dir is not None: alt_work_dir = alt_work_dir.strip() active = etree_file.get("active", "true").strip() prepare_obj = jube2.fileset.Prepare(cmd, stdout_filename, stderr_filename, alt_work_dir, active) filelist.append(prepare_obj) return filelist def _extract_substitutesets(self, etree): """Extract substitutesets from benchmark Return a dict of substitute sets, e.g. {"compilesub": ([iofile0,...], [sub0,...])}""" substitutesets = dict() for element in etree.findall("substituteset"): name = Parser._attribute_from_element(element, "name").strip() if name == "": raise ValueError("Empty \"name\" attribute in " + " found.") LOGGER.debug(" Parsing ".format(name)) init_with = element.get("init_with") files, subs = Parser._extract_subs(element) if name in substitutesets: raise ValueError("\"{0}\" not unique".format(name)) if init_with is not None: parts = init_with.strip().split(":") if len(parts) > 1: search_name = parts[1] else: search_name = None substitutesets[name] = \ self._extract_extern_set(parts[0], "substituteset", name, search_name) substitutesets[name].update_files(files) substitutesets[name].update_substitute(subs) else: substitutesets[name] = \ jube2.substitute.Substituteset(name, files, subs) return substitutesets @staticmethod def _extract_subs(etree_substituteset): """Extract files for substitution and subs from substituteset Return a files dict for substitute and a dict of subs """ valid_tags = ["iofile", "sub"] files = list() subs = dict() for sub in etree_substituteset: Parser._check_tag(sub, valid_tags) if sub.tag == "iofile": in_file = Parser._attribute_from_element(sub, "in").strip() out_file = Parser._attribute_from_element( sub, "out").strip() out_mode = sub.get("out_mode", "w").strip() if out_mode not in ["w", "a"]: raise ValueError( "out_mode in must be \"w\" or \"a\"") in_file = os.path.expandvars(os.path.expanduser(in_file)) out_file = os.path.expandvars(os.path.expanduser(out_file)) files.append((out_file, in_file, out_mode)) elif sub.tag == "sub": source = "" + \ Parser._attribute_from_element(sub, "source").strip() if source == "": raise ValueError( "Empty \"source\" attribute in found.") dest = sub.get("dest") if dest is None: dest = sub.text if dest is None: dest = "" dest = dest.strip() + "" subs[source] = dest return (files, subs) @staticmethod def _attribute_from_element(element, attribute): """Return attribute from element element -- etree.Element attribute -- string Raise a useful exception if value not found """ value = element.get(attribute) if value is None: raise ValueError("Missing attribute '{0}' in <{1}>" .format(attribute, element.tag)) return value @staticmethod def _check_tag(element, valid_tags): """Check tag and raise a useful exception if needed element -- etree.Element valid_tags -- list of valid strings """ if element.tag not in valid_tags: raise ValueError(("Unknown tag or tag used in wrong " + "position:\n{0}").format( jube2.util.output.element_tree_tostring( element, encoding="UTF-8"))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/log.py0000644000175000017500000001301000000000000015604 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Logging Support""" from __future__ import (print_function, unicode_literals, division) import logging import sys import glob import os.path import jube2.conf class JubeLogger(logging.getLoggerClass(), object): """Overwrite logging to handle multi line messages.""" def _log(self, level, msg, *args, **kwargs): """Log multi line messages each as a separate entry.""" if hasattr(msg, "splitlines"): lines = msg.splitlines() else: lines = str(msg).splitlines() for line in lines: super(JubeLogger, self)._log(level, line, *args, **kwargs) logging.setLoggerClass(JubeLogger) LOGGING_MODE = jube2.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube2.conf.DEFAULT_LOGFILE_NAME CONSOLE_VERBOSE = False def get_logger(name=None): """Return logger given by name""" return logging.getLogger(name) def setup_logging(mode=None, filename=None, verbose=None): """Setup the logging configuration. Available modes are default log to console and file console only console output filename can be given optionally. verbose: enable verbose console output The setup includes setting the handlers and formatters. Calling this function multiple times causes old handlers to be removed before new ones are added. """ global LOGGING_MODE, LOGFILE_NAME, CONSOLE_VERBOSE # Use debug file name and debug file mode when in debug mode if jube2.conf.DEBUG_MODE: filename = jube2.conf.LOGFILE_DEBUG_NAME mode = "default" filemode = jube2.conf.LOGFILE_DEBUG_MODE else: filemode = "a" if mode is None: mode = LOGGING_MODE else: LOGGING_MODE = mode if filename is None: filename = LOGFILE_NAME else: LOGFILE_NAME = filename if verbose is None: verbose = CONSOLE_VERBOSE else: CONSOLE_VERBOSE = verbose # this is needed to make the other handlers accept on low priority # events _logger = get_logger("jube2") _logger.setLevel(logging.DEBUG) # list is needed since we remove from the list we just iterate # over for handler in list(_logger.handlers): handler.close() _logger.removeHandler(handler) # create, configure and add console handler console_formatter = logging.Formatter(jube2.conf.LOG_CONSOLE_FORMAT) console_handler = logging.StreamHandler(sys.stdout) if verbose: console_handler.setLevel(logging.DEBUG) else: console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_formatter) _logger.addHandler(console_handler) if mode == "default": try: # create, configure and add file handler file_formatter = logging.Formatter(jube2.conf.LOG_FILE_FORMAT) file_handler = logging.FileHandler(filename, filemode) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) _logger.addHandler(file_handler) except IOError: pass def search_for_logs(path=None): """Search for files matching in path with .log extension""" if path is None: path = "." matches = glob.glob(os.path.join(path, "*.log")) return matches def log_print(text): """Output text""" print(text) def matching_logs(commands, available_logs): """Find intersection between requested logs and available logs. Returns tuple (matching, not_matching), containing the intersection and its complement. Only compares basenames. """ requested_logs = set("{0}.log".format(command) for command in commands) matching = list() for log in available_logs: if os.path.basename(log) in requested_logs: matching.append(log) not_matching = requested_logs.difference(set([os.path.basename(log) for log in matching])) return matching, not_matching def safe_output_logfile(filename): """Try to print logfile. If try fails, fail gracefully.""" try: with open(filename) as logfile: log_print(logfile.read()) except IOError: log_print("No log found in current directory") def change_logfile_name(filename): """Change log file name if not in debug mode.""" if jube2.conf.DEBUG_MODE: return setup_logging(filename=filename, mode="default") def only_console_log(): """Change to console log if not in debug mode.""" if jube2.conf.DEBUG_MODE: return setup_logging(mode="console") def reset_logging(): """Reset logging to default.""" global LOGGING_MODE, LOGFILE_NAME LOGGING_MODE = jube2.conf.DEFAULT_LOGGING_MODE LOGFILE_NAME = jube2.conf.DEFAULT_LOGFILE_NAME ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/main.py0000644000175000017500000010704200000000000015760 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """CLI program""" from __future__ import (print_function, unicode_literals, division) import jube2.jubeio import jube2.util.util import jube2.util.output import jube2.conf import jube2.info import jube2.help import jube2.log import jube2.completion import sys import os import re import shutil from distutils.version import StrictVersion try: from urllib.request import urlopen except ImportError: from urllib import urlopen try: import argparse except ImportError: print("argparse module not available; either install it " "(https://pypi.python.org/pypi/argparse), or " "switch to a Python version that includes it.") sys.exit(1) LOGGER = jube2.log.get_logger(__name__) def continue_benchmarks(args): """Continue benchmarks""" found_benchmarks = search_for_benchmarks(args) jube2.conf.HIDE_ANIMATIONS = args.hide_animation for benchmark_folder in found_benchmarks: _continue_benchmark(benchmark_folder, args) def status(args): """Show benchmark status""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: benchmark = _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: return jube2.info.print_benchmark_status(benchmark) def benchmarks_results(args): """Show benchmark results""" found_benchmarks = search_for_benchmarks(args) result_list = list() # Start with the newest benchmark to set the newest result configuration found_benchmarks.reverse() cnt = 0 for benchmark_folder in found_benchmarks: if (args.num is None) or (cnt < args.num): result_list = _benchmark_result(benchmark_folder=benchmark_folder, args=args, result_list=result_list) cnt += 1 for result_data in result_list: result_data.create_result(reverse=args.reverse) def analyse_benchmarks(args): """Analyse benchmarks""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _analyse_benchmark(benchmark_folder, args) def remove_benchmarks(args): """Remove benchmarks or workpackages""" if(args.workpackage is not None): # If a workpackage id is provided by the user, only specific # workpackages will be removed found_workpackages = search_for_workpackage(args) for workpackage in found_workpackages: _remove_workpackage(workpackage, args) else: # Delete complete benchmarks found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _remove_benchmark(benchmark_folder, args) def command_help(args): """Show command help""" subparser = _get_args_parser()[1] if args.command is None: subparser["help"].print_help() elif args.command.lower() == "all": for key in sorted(jube2.help.HELP.keys()): print("{0}:".format(key)) print(jube2.help.HELP[key]) else: if args.command in jube2.help.HELP: if args.command in subparser: subparser[args.command].print_help() else: print(jube2.help.HELP[args.command]) else: print("no help found for {0}".format(args.command)) subparser["help"].print_help() def info(args): """Benchmark information""" if args.id is None: jube2.info.print_benchmarks_info(args.dir) else: found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: benchmark = \ _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: continue if args.step is None: jube2.info.print_benchmark_info(benchmark) else: if args.step: steps = args.step else: steps = benchmark.steps.keys() # Set default csv_parametrization value to allow empty -c # option if args.csv_parametrization is None: args.csv_parametrization = "," for step_name in steps: jube2.info.print_step_info( benchmark, step_name, parametrization_only=args.parametrization, parametrization_only_csv=args.csv_parametrization) def update_check(args): """Check if a newer JUBE version is available.""" try: website = urlopen(jube2.conf.UPDATE_VERSION_URL) version = website.read().decode().strip() if StrictVersion(jube2.conf.JUBE_VERSION) >= StrictVersion(version): LOGGER.info("Newest JUBE version {0} is already " "installed.".format(jube2.conf.JUBE_VERSION)) else: LOGGER.info(("Newer JUBE version {0} is available. " "Currently installed version is {1}.\n" "New version can be " "downloaded here: {2}").format( version, jube2.conf.JUBE_VERSION, jube2.conf.UPDATE_URL)) except IOError as ioe: raise IOError("Cannot connect to {0}: {1}".format( jube2.conf.UPDATE_VERSION_URL, str(ioe))) except ValueError as verr: raise ValueError("Cannot read version string from {0}: {1}".format( jube2.conf.UPDATE_VERSION_URL, str(verr))) def show_log(args): """Show logs for benchmarks""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: show_log_single(args, benchmark_folder) def show_log_single(args, benchmark_folder): """Show logs for a single benchmark""" # Find available logs available_logs = jube2.log.search_for_logs(benchmark_folder) # Use all available logs if none is selected ... if not args.command: matching = available_logs not_matching = list() # ... otherwise find intersection between available and # selected else: matching, not_matching = jube2.log.matching_logs( args.command, available_logs) # Output the log file for log in matching: jube2.log.log_print("BenchmarkID: {0} | Log: {1}".format( int(os.path.basename(benchmark_folder)), log)) jube2.log.safe_output_logfile(log) # Inform user if any selected log was not found if not_matching: jube2.log.log_print("Could not find logs: {0}".format( ",".join(not_matching))) def complete(args): """Handle shell completion""" jube2.completion.complete_function_bash(args) def _load_existing_benchmark(args, benchmark_folder, restore_workpackages=True, load_analyse=True): """Load an existing benchmark, given by directory benchmark_folder.""" jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_PARSE_NAME)) # Add log information LOGGER.debug("Command: {0} {1}".format( os.path.basename(sys.argv[0]), " ".join(sys.argv[1:]))) LOGGER.debug("Version: {0}".format(jube2.conf.JUBE_VERSION)) # Read existing benchmark configuration try: parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.CONFIGURATION_FILENAME), force=args.force, strict=args.strict) benchmarks = parser.benchmarks_from_xml()[0] except IOError as exeption: LOGGER.warning(str(exeption)) return None # benchmarks can be None if version conflict was blocked if benchmarks is not None: # Only one single benchmark exist inside benchmarks benchmark = list(benchmarks.values())[0] else: return None # Restore old benchmark id benchmark.id = int(os.path.basename(benchmark_folder)) if restore_workpackages: # Read existing workpackage information try: parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.WORKPACKAGES_FILENAME), force=args.force, strict=args.strict) workpackages, work_stat = parser.workpackages_from_xml(benchmark) except IOError as exeption: LOGGER.warning(str(exeption)) return None benchmark.set_workpackage_information(workpackages, work_stat) if load_analyse and os.path.isfile(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME)): # Read existing analyse data parser = jube2.jubeio.Parser(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME), force=args.force, strict=args.strict) analyse_result = parser.analyse_result_from_xml() if analyse_result is not None: for analyser in benchmark.analyser.values(): if analyser.name in analyse_result: analyser.analyse_result = analyse_result[analyser.name] jube2.log.only_console_log() return benchmark def manipulate_comments(args): """Manipulate benchmark comment""" found_benchmarks = search_for_benchmarks(args) for benchmark_folder in found_benchmarks: _manipulate_comment(benchmark_folder, args) def search_for_benchmarks(args): """Search for existing benchmarks""" found_benchmarks = list() if not os.path.isdir(args.dir): raise OSError("Not a directory: \"{0}\"".format(args.dir)) all_benchmarks = [ os.path.join(args.dir, directory) for directory in os.listdir(args.dir) if os.path.isdir(os.path.join(args.dir, directory))] all_benchmarks.sort() if (args.id is not None) and ("all" not in args.id): for benchmark_id in args.id: if benchmark_id == "last": benchmark_id = jube2.util.util.get_current_id(args.dir) # Search for existing benchmark benchmark_id = int(benchmark_id) if benchmark_id < 0: benchmark_id = int( os.path.basename(all_benchmarks[benchmark_id])) benchmark_folder = jube2.util.util.id_dir(args.dir, benchmark_id) if not os.path.isdir(benchmark_folder): raise OSError("Benchmark directory not found: \"{0}\"" .format(benchmark_folder)) if not os.path.isfile(os.path.join( benchmark_folder, jube2.conf.CONFIGURATION_FILENAME)): LOGGER.warning(("Configuration file \"{0}\" not found in " + "\"{1}\" or directory not readable.") .format(jube2.conf.CONFIGURATION_FILENAME, benchmark_folder)) if benchmark_folder not in found_benchmarks: found_benchmarks.append(benchmark_folder) else: if (args.id is not None) and ("all" in args.id): # Add all available benchmark folder found_benchmarks = all_benchmarks else: # Get highest benchmark id and build benchmark_folder benchmark_id = jube2.util.util.get_current_id(args.dir) benchmark_folder = jube2.util.util.id_dir(args.dir, benchmark_id) if os.path.isdir(benchmark_folder): found_benchmarks.append(benchmark_folder) else: raise OSError("No benchmark directory found in \"{0}\"" .format(args.dir)) found_benchmarks = \ [benchmark_folder for benchmark_folder in found_benchmarks if os.path.isfile(os.path.join(benchmark_folder, jube2.conf.CONFIGURATION_FILENAME))] found_benchmarks.sort() return found_benchmarks def search_for_workpackage(args): """Search for 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: for wp_id in args.workpackage: if benchmark.workpackage_by_id(int(wp_id)) is None: raise RuntimeError(("No workpackage \"{0}\" found " + "in benchmark \"{1}\".") .format(wp_id, benchmark.id)) else: found_workpackages.append( benchmark.workpackage_by_id(int(wp_id))) return found_workpackages def run_new_benchmark(args): """Start a new benchmark run""" jube2.conf.HIDE_ANIMATIONS = args.hide_animation jube2.conf.EXIT_ON_ERROR = args.error id_cnt = 0 # Extract tags tags = args.tag if tags is not None: tags = set(tags) for path in args.files: # Setup Logging jube2.log.change_logfile_name( filename=os.path.join(os.path.dirname(path), jube2.conf.DEFAULT_LOGFILE_NAME)) # Add log information LOGGER.debug("Command: {0} {1}".format( os.path.basename(sys.argv[0]), " ".join(sys.argv[1:]))) LOGGER.debug("Version: {0}".format(jube2.conf.JUBE_VERSION)) # Read new benchmarks if args.include_path is not None: include_pathes = [include_path for include_path in args.include_path if include_path != ""] else: include_pathes = None parser = jube2.jubeio.Parser(path, tags, include_pathes, args.force, args.strict) benchmarks, only_bench, not_bench = parser.benchmarks_from_xml() # Add new comment if args.comment is not None: for benchmark in benchmarks.values(): benchmark.comment = re.sub(r"\s+", " ", args.comment) # CLI input overwrite fileinput if args.only_bench: only_bench = args.only_bench if args.not_bench: not_bench = args.not_bench # No specific -> do all if len(only_bench) == 0 and benchmarks is not None: only_bench = list(benchmarks) for bench_name in only_bench: if bench_name in not_bench: continue bench = benchmarks[bench_name] # Set user defined id if (args.id is not None) and (len(args.id) > id_cnt): if args.id[id_cnt] < 0: LOGGER.warning("Negative ids are not allowed. Skipping id " "'{}'.".format(args.id[id_cnt])) id_cnt += 1 continue bench.id = args.id[id_cnt] id_cnt += 1 # Change runtime outpath if specified if args.outpath is not None: bench.outpath = args.outpath # Start benchmark run bench.new_run() # Run analyse if args.analyse or args.result: jube2.log.change_logfile_name(os.path.join( bench.bench_dir, jube2.conf.LOGFILE_ANALYSE_NAME)) bench.analyse() # Create result data if args.result: jube2.log.change_logfile_name(os.path.join( bench.bench_dir, jube2.conf.LOGFILE_RESULT_NAME)) bench.create_result(show=True) # Clean up when using debug mode if jube2.conf.DEBUG_MODE: bench.delete_bench_dir() # Reset logging jube2.log.only_console_log() def _continue_benchmark(benchmark_folder, args): """Continue existing benchmark""" jube2.conf.EXIT_ON_ERROR = args.error benchmark = _load_existing_benchmark(args, benchmark_folder) if benchmark is None: return # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_CONTINUE_NAME)) # Run existing benchmark benchmark.run() # Run analyse if args.analyse or args.result: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse() # Create result data if args.result: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_RESULT_NAME)) benchmark.create_result(show=True) # Clean up when using debug mode if jube2.conf.DEBUG_MODE: benchmark.reset_all_workpackages() # Reset logging jube2.log.only_console_log() def _analyse_benchmark(benchmark_folder, args): """Analyse existing benchmark""" benchmark = _load_existing_benchmark(args, benchmark_folder, load_analyse=False) if benchmark is None: return # Update benchmark data _update_analyse_and_result(args, benchmark) # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) LOGGER.info(jube2.util.output.text_boxed( ("Analyse benchmark \"{0}\" id: {1}").format(benchmark.name, benchmark.id))) benchmark.analyse() if os.path.isfile( os.path.join(benchmark_folder, jube2.conf.ANALYSE_FILENAME)): LOGGER.info(">>> Analyse data storage: {0}".format(os.path.join( benchmark_folder, jube2.conf.ANALYSE_FILENAME))) else: LOGGER.info(">>> Analyse data storage \"{0}\" not created!".format( os.path.join(benchmark_folder, jube2.conf.ANALYSE_FILENAME))) LOGGER.info(jube2.util.output.text_line()) # Reset logging jube2.log.only_console_log() def _benchmark_result(benchmark_folder, args, result_list=None): """Show benchmark result""" benchmark = _load_existing_benchmark(args, benchmark_folder) if result_list is None: result_list = list() if benchmark is None: return result_list if (args.update is None) and (args.tag is not None) and \ (len(benchmark.tags & set(args.tag)) == 0): return result_list # Update benchmark data _update_analyse_and_result(args, benchmark) # Run benchmark analyse if args.analyse: jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_ANALYSE_NAME)) benchmark.analyse(show_info=False) # Change logfile jube2.log.change_logfile_name(os.path.join( benchmark_folder, jube2.conf.LOGFILE_RESULT_NAME)) # Create benchmark results result_list = benchmark.create_result(only=args.only, data_list=result_list, style=args.style) # Reset logging jube2.log.only_console_log() return result_list def _update_analyse_and_result(args, benchmark): """Update analyse and result data in given benchmark by using the given update file""" if args.update is not None: dirname = os.path.dirname(args.update) # Extract tags benchmark.add_tags(args.tag) tags = benchmark.tags # Read new benchmarks if args.include_path is not None: include_pathes = [include_path for include_path in args.include_path if include_path != ""] else: include_pathes = None parser = jube2.jubeio.Parser(args.update, tags, include_pathes, args.force, args.strict) benchmarks = parser.benchmarks_from_xml()[0] # Update benchmark for bench in benchmarks.values(): if bench.name == benchmark.name: benchmark.update_analyse_and_result(bench.patternsets, bench.analyser, bench.results, bench.results_order, dirname) break else: LOGGER.debug(("No benchmark data for benchmark {0} was found " + "while running update.").format(benchmark.name)) def _remove_benchmark(benchmark_folder, args): """Remove existing benchmark""" remove = True if not args.force: try: inp = raw_input("Really remove \"{0}\" (y/n):" .format(benchmark_folder)) except NameError: inp = input("Really remove \"{0}\" (y/n):" .format(benchmark_folder)) remove = inp.startswith("y") if remove: # Delete benchmark folder shutil.rmtree(benchmark_folder, ignore_errors=True) def _remove_workpackage(workpackage, args): """Remove existing workpackages""" remove = True # Ignore deleted/unstarted workpackages if workpackage.started: if not args.force: try: inp = raw_input(("Really remove \"{0}\" and its dependent " + "workpackages (y/n):") .format(workpackage.workpackage_dir)) except NameError: inp = input(("Really remove \"{0}\" and its dependent " + "workpackages (y/n):") .format(workpackage.workpackage_dir)) remove = inp.startswith("y") if remove: workpackage.remove() workpackage.benchmark.write_workpackage_information( os.path.join(workpackage.benchmark.bench_dir, jube2.conf.WORKPACKAGES_FILENAME)) def _manipulate_comment(benchmark_folder, args): """Change or append the comment in given benchmark.""" benchmark = _load_existing_benchmark(args, benchmark_folder=benchmark_folder, restore_workpackages=False, load_analyse=False) if benchmark is None: return # Change benchmark comment if args.append: comment = benchmark.comment + args.comment else: comment = args.comment benchmark.comment = re.sub(r"\s+", " ", comment) benchmark.write_benchmark_configuration( os.path.join(benchmark_folder, jube2.conf.CONFIGURATION_FILENAME), outpath="..") def gen_parser_conf(): """Generate dict with parser information""" config = ( (("-V", "--version"), {"help": "show version", "action": "version", "version": "JUBE, version {0}".format( jube2.conf.JUBE_VERSION)}), (("-v", "--verbose"), {"help": "enable verbose console output (use -vv to " + "show stdout during execution and -vvv to " + "show log and stdout)", "action": "count", "default": 0}), (("--debug",), {"action": "store_true", "help": 'use debugging mode'}), (("--force",), {"action": "store_true", "help": 'skip version check'}), (("--strict",), {"action": "store_true", "help": 'force need for correct version'}), (("--devel",), {"action": "store_true", "help": 'show development related information'}) ) return config def gen_subparser_conf(): """Generate dict with subparser information""" subparser_configuration = dict() # run subparser subparser_configuration["run"] = { "help": "processes benchmark", "func": run_new_benchmark, "arguments": { ("files",): {"metavar": "FILE", "nargs": "+", "help": "input file"}, ("--only-bench",): {"nargs": "+", "help": "only run benchmark"}, ("--not-bench",): {"nargs": "+", "help": "do not run benchmark"}, ("-t", "--tag"): {"nargs": "+", "help": "select tags"}, ("-i", "--id"): {"type": int, "help": "use specific benchmark id", "nargs": "+"}, ("-e", "--error"): {"action": "store_true", "help": "exit on error"}, ("--hide-animation",): {"action": "store_true", "help": "hide animations"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse"}, ("-r", "--result"): {"action": "store_true", "help": "show results"}, ("-m", "--comment"): {"help": "add comment"}, ("-o", "--outpath"): {"help": "overwrite outpath directory"} } } # continue subparser subparser_configuration["continue"] = { "help": "continue benchmark", "func": continue_benchmarks, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("--hide-animation",): {"action": "store_true", "help": "hide animations"}, ("-e", "--error"): {"action": "store_true", "help": "exit on error"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse"}, ("-r", "--result"): {"action": "store_true", "help": "show results"} } } # analyse subparser subparser_configuration["analyse"] = { "help": "analyse benchmark", "func": analyse_benchmarks, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-u", "--update"): {"metavar": "UPDATE_FILE", "help": "update analyse and result configuration"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-t", "--tag"): {"nargs": "+", "help": "select tags"} } } # result subparser subparser_configuration["result"] = { "help": "show benchmark results", "func": benchmarks_results, "arguments": { ("dir",): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "nargs": "+"}, ("-a", "--analyse"): {"action": "store_true", "help": "run analyse before creating result"}, ("-u", "--update"): {"metavar": "UPDATE_FILE", "help": "update analyse and result configuration"}, ("--include-path",): {"nargs": "+", "help": "directory containing include files"}, ("-t", "--tag"): {"nargs": '+', "help": "select tags"}, ("-o", "--only"): {"nargs": "+", "metavar": "RESULT_NAME", "help": "only create results given by specific name"}, ("-r", "--reverse"): {"help": "reverse benchmark output order", "action": "store_true"}, ("-n", "--num"): {"type": int, "help": "show only last N benchmarks"}, ("-s", "--style"): {"help": "overwrites table style type", "choices": ["pretty", "csv", "aligned"]} } } # 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"} } } # 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": "+"} } } # 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 " 'usage: eval "$(jube complete)"', "func": complete, "arguments": { ('--command-name', "-c"): {"nargs": 1, "help": "name of command to be completed", "default": [os.path.basename(sys.argv[0])]}, } } return subparser_configuration def _get_args_parser(): """Create argument parser""" parser = argparse.ArgumentParser() for args, kwargs in gen_parser_conf(): parser.add_argument(*args, **kwargs) subparsers = parser.add_subparsers(dest="subparser", help='subparsers') subparser_configuration = gen_subparser_conf() # create subparser out of subparser configuration subparser = dict() for name, subparser_config in subparser_configuration.items(): subparser[name] = \ subparsers.add_parser( name, help=subparser_config.get("help", ""), description=jube2.help.HELP.get(name, ""), formatter_class=argparse.RawDescriptionHelpFormatter) subparser[name].set_defaults(func=subparser_config["func"]) if "arguments" in subparser_config: for names, arg in subparser_config["arguments"].items(): subparser[name].add_argument(*names, **arg) # create help key word overview help_keys = sorted(list(jube2.help.HELP) + ["ALL"]) max_word_length = max(map(len, help_keys)) + 4 # calculate max number of keyword columns max_columns = jube2.conf.DEFAULT_WIDTH // max_word_length # fill keyword list to match number of columns help_keys += [""] * (len(help_keys) % max_columns) help_keys = list(zip(*[iter(help_keys)] * max_columns)) # create overview help_overview = jube2.util.output.text_table(help_keys, separator=" ", align_right=False) # help subparser subparser["help"] = \ subparsers.add_parser( 'help', help='command help', formatter_class=argparse.RawDescriptionHelpFormatter, description="available commands or info elements: \n" + help_overview) subparser["help"].add_argument('command', nargs='?', help="command or info element") subparser["help"].set_defaults(func=command_help) return parser, subparser def main(command=None): """Parse the command line and run the requested command.""" jube2.help.load_help() parser = _get_args_parser()[0] if command is None: args = parser.parse_args() else: args = parser.parse_args(command) jube2.conf.DEBUG_MODE = args.debug jube2.conf.VERBOSE_LEVEL = args.verbose if jube2.conf.VERBOSE_LEVEL > 0: args.hide_animation = True # Set new umask if JUBE_GROUP_NAME is used current_mask = os.umask(0) if (jube2.util.util.check_and_get_group_id() is not None) and \ (current_mask > 2): current_mask = 2 os.umask(current_mask) if args.subparser: jube2.log.setup_logging(mode="console", verbose=(jube2.conf.VERBOSE_LEVEL == 1) or (jube2.conf.VERBOSE_LEVEL == 3)) if args.devel: args.func(args) else: try: args.func(args) except Exception as exeption: # Catch all possible Exceptions LOGGER.error("\n" + str(exeption)) jube2.log.reset_logging() exit(1) else: parser.print_usage() jube2.log.reset_logging() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/parameter.py0000644000175000017500000010651400000000000017017 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Parameter related classes""" from __future__ import (print_function, unicode_literals, division) import itertools import os import xml.etree.ElementTree as ET import copy import jube2.util.util import jube2.conf import jube2.log import re import inspect LOGGER = jube2.log.get_logger(__name__) JUBE_MODE = "jube" NEVER_MODE = "never" STEP_MODE = "step" CYCLE_MODE = "cycle" ALWAYS_MODE = "always" USE_MODE = "use" UPDATE_MODES = (JUBE_MODE, NEVER_MODE, STEP_MODE, CYCLE_MODE, USE_MODE, ALWAYS_MODE) class Parameterset(object): """A parameterset represent a template or a specific product space. It can be combined with other Parametersets.""" def __init__(self, name="", duplicate="replace"): self._name = name self._duplicate = duplicate self._parameters = dict() def clear(self): """Remove all stored parameters""" self._parameters = dict() def copy(self): """Returns a deepcopy of the Parameterset""" new_parameterset = Parameterset(self._name, self._duplicate) new_parameterset.add_parameterset(self) return new_parameterset @property def name(self): """Return name of the Parameterset""" return self._name @property def duplicate(self): """Return the duplicate property of the Parameterset""" return self._duplicate @property def has_templates(self): """This Parameterset contains template paramters?""" for parameter in self._parameters.values(): if parameter.is_template: return True return False @property def parameter_dict(self): """Return dictionary name -> parameter""" return dict(self._parameters) @property def all_parameters(self): """Return list of all parameters""" return self._parameters.values() @property def all_parameter_names(self): """Return list of all parameter names""" return self._parameters.keys() def add_parameterset(self, parameterset): """Add all parameters from given parameterset, existing ones will be overwritten""" for parameter in parameterset: self.add_parameter(parameter.copy()) return self def update_parameterset(self, parameterset): """Overwrite existing parameters. Do not add new parameters""" for parameter in parameterset: if parameter.name in self: self._parameters[parameter.name] = parameter.copy() def concat_parameter(self, parameter): """Concatenate a new parameter to a potentially existing one.""" if parameter.name in self._parameters.keys(): if self._parameters[parameter.name]._value == parameter._value: return parameter else: value=list(set(jube2.util.util.ensure_list(self._parameters[parameter.name]._value)+jube2.util.util.ensure_list(parameter._value))) value.sort() return jube2.parameter.TemplateParameter( parameter._name, value, parameter._separator, parameter._type, parameter._mode, parameter._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: 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 or \ parameter._duplicate != self._parameters[parameter.name]._duplicate: raise ValueError("At least one option for the parameter {0} was defined at least twice differently leading to undefined behaviour.".format(parameter.name)) def add_parameter(self, parameter): """Add a new parameter""" if parameter.name not in self._parameters.keys(): self._parameters[parameter.name] = parameter else: # Check whether only the duplicate option of two parameters is # identical, otherwise the behaviour is undefined. self.check_parameter_options(parameter=parameter, only_duplicate=True) # check, which action to perform and prioritize the duplicate # option from the parameters over the duplicate option from # the parametersets raise_unknown_error=False if parameter._duplicate == "replace": self._parameters[parameter.name] = parameter elif parameter._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif parameter._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter elif parameter._duplicate == "none": if self._duplicate == "replace": self._parameters[parameter.name] = parameter elif self._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif self._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter else: # unknown error, this situation should never occur! raise_unknown_error=True else: # unknown error, this situation should never occur! raise_unknown_error=True if raise_unknown_error: raise Exception("The execution was aborted due to an unknown error "+ "when adding a parameter. Please contact the JUBE developers "+ "to resolve this situation.") def delete_parameter(self, parameter): """Delete a parameter""" name = "" if isinstance(parameter, Parameter): name = parameter.name else: name = parameter if name in self._parameters: del self._parameters[name] @property def constant_parameter_dict(self): """Return dictionary representation of all constant parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and (parameter.mode not in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES))]) @property def template_parameter_dict(self): """Return dictionary representation of all template parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if parameter.is_template]) @property def export_parameter_dict(self): """Return dictionary representation of all export parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and parameter.export]) def get_updatable_parameter(self, mode, keep_index=False): """Returns a parameterset containing all updatable parameter for a specific mode, the root parameter is added""" parameterset = Parameterset() for parameter in self._parameters.values(): if ((parameter.update_mode == mode) or (parameter.update_mode == ALWAYS_MODE and mode == CYCLE_MODE) or (parameter.update_mode == STEP_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == STEP_MODE)): root_paramter = parameter.based_on_root.copy() if keep_index: root_paramter.idx = parameter.idx parameterset.add_parameter(root_paramter) return parameterset def is_compatible(self, parameterset, update_mode=NEVER_MODE): """Two Parametersets are compatible, if the intersection only contains equivilant parameters""" return len(self.get_incompatible_parameter( parameterset, update_mode)) == 0 def get_incompatible_parameter(self, parameterset, update_mode=NEVER_MODE): """Return a set of incompatible parameter names between the current and the given parameterset""" result = set() # Find parameternames which exists in both parametersets intersection = set(self.all_parameter_names) & \ set(parameterset.all_parameter_names) for name in intersection: if (not (self[name].update_allowed(update_mode) or # In case of the USE_MODE (in the beginning of a # new step) only the actual new parameterset and its # mode is relevant parameterset[name].update_allowed( NEVER_MODE if (update_mode == USE_MODE) else update_mode)) and not self[name].is_equivalent(parameterset[name])): result.add(name) return result def remove_jube_parameter(self): """Remove JUBE update mode parameter from the parameterset""" remove_list = [] for parameter in self: if parameter.is_jube_parameter: remove_list.append(parameter.name) for parameter_name in remove_list: self.delete_parameter(parameter_name) def expand_templates(self): """Expand all remaining templates in the Parameterset and returns the resulting parametersets """ parameter_list = list() # Create all possible constant parameter representations for parameter in self.template_parameter_dict.values(): expanded_parameter_list = list() for static_param in parameter.expand(): expanded_parameter_list.append(static_param) parameter_list.append(expanded_parameter_list) # Generator for parameters in itertools.product(*parameter_list): parameterset = self.copy() # Addition of the constant parameters will overwrite the templates for parameter in parameters: parameterset.add_parameter(parameter) yield parameterset def __contains__(self, parameter): if isinstance(parameter, Parameter): if parameter.name in self._parameters: return parameter.is_equivalent( self._parameters[parameter.name]) else: return False else: return parameter in self._parameters def __getitem__(self, name): if name in self._parameters: return self._parameters[name] else: return None def __iter__(self): for parameter in self.all_parameters: yield parameter def etree_repr(self, use_current_selection=False): """Return etree object representation""" parameterset_etree = ET.Element('parameterset') if len(self._name) > 0: parameterset_etree.attrib["name"] = self._name parameterset_etree.attrib["duplicate"] = self._duplicate for parameter in self._parameters.values(): parameterset_etree.append( parameter.etree_repr(use_current_selection)) return parameterset_etree def __len__(self): return len(self._parameters) def __repr__(self): return "Parameterset:{0}".format( dict([[parameter.name, parameter.value] for parameter in self.all_parameters])) def parameter_substitution(self, additional_parametersets=None, final_sub=False): """Substitute all parameter inside the parameterset. Parameters from additional_parameterset will be used for substitution but will not be added to the set. final_sub marks the last substitution process.""" set_changed = True count = 0 while set_changed and (not self.has_templates) and \ (count < jube2.conf.MAX_RECURSIVE_SUB): set_changed = False count += 1 # Create dependencies depend_dict = dict() for par in self: if not par.is_template: depend_dict[par.name] = set() for other_par in self: # search for parameter usage if par.depends_on(other_par): depend_dict[par.name].add(other_par.name) # Resolve dependencies substitution_list = [self._parameters[name] for name in jube2.util.util.resolve_depend(depend_dict)] # Do substition and evaluation if possible set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets) # Run forced evaluation if there were no further changes if not set_changed: set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets, force_evaluation=True) if final_sub: parameter = [par for par in self] for par in parameter: if par.is_template: LOGGER.debug( ("Parameter ${0} = {1} is handled as " + "a template and will not be evaluated.\n").format( par.name, par.value)) else: new_par, param_changed = \ par.substitute_and_evaluate(final_sub=True) if param_changed: self.add_parameter(new_par) def __substitute_parameters_in_list(self, parameter_list, additional_parametersets=None, force_evaluation=False): """Substitute all parameter inside the given parameter_list. Parameters from additional_parameterset will be used for substitution but will not be added to the set. force_evaluation will force script parameter evaluation""" set_changed = False for par in parameter_list: if par.can_substitute_and_evaluate(self): parametersets = [self] if additional_parametersets is not None: parametersets += additional_parametersets new_par, param_changed = \ par.substitute_and_evaluate( parametersets, force_evaluation=force_evaluation) if param_changed: self.add_parameter(new_par) set_changed = set_changed or param_changed return set_changed class Parameter(object): """Contains data for single Parameter. This Parameter can be a constant value, a template or a specific value out of a given template""" # This regex can be used to find variables inside parameter values parameter_regex = \ re.compile(r"(?. """Patternset definition""" from __future__ import (print_function, unicode_literals, division) import jube2.parameter import xml.etree.ElementTree as ET LOGGER = jube2.log.get_logger(__name__) class Patternset(object): """A Patternset stores a set of pattern and derived pattern.""" def __init__(self, name=""): self._name = name self._pattern = jube2.parameter.Parameterset("pattern") self._derived_pattern = jube2.parameter.Parameterset("derived_pattern") def add_pattern(self, pattern): """Add a additional pattern to the patternset. Existing pattern using the same name will be overwritten""" if pattern.derived: if pattern in self._pattern: self._pattern.delete_parameter(pattern) self._derived_pattern.add_parameter(pattern) else: if pattern in self._derived_pattern: self._derived_pattern.delete_parameter(pattern) self._pattern.add_parameter(pattern) @property def pattern_storage(self): """Return the pattern storage""" return self._pattern @property def derived_pattern_storage(self): """Return the derived pattern storage""" return self._derived_pattern def etree_repr(self): """Return etree object representation""" patternset_etree = ET.Element('patternset') patternset_etree.attrib["name"] = self._name for pattern in self._pattern: patternset_etree.append( pattern.etree_repr()) for pattern in self._derived_pattern: patternset_etree.append( pattern.etree_repr()) return patternset_etree def add_patternset(self, patternset): """Add all pattern from given patternset to the current one""" self._pattern.add_parameterset(patternset.pattern_storage) self._derived_pattern.add_parameterset( patternset.derived_pattern_storage) def pattern_substitution(self, parametersets=None): """Run pattern substitution using additional parameterset""" if parametersets is None: parametersets = list() self._pattern.parameter_substitution( additional_parametersets=parametersets, final_sub=True) def derived_pattern_substitution(self, parametersets=None): """Run derived pattern substitution using additional parameterset""" if parametersets is None: parametersets = list() self._derived_pattern.parameter_substitution( additional_parametersets=parametersets, final_sub=True) @property def name(self): """Get patternset name""" return self._name def copy(self): """Returns a copy of the Parameterset""" new_patternset = Patternset(self._name) new_patternset.add_patternset(self) return new_patternset def is_compatible(self, patternset): """Two Patternsets are compatible, if all pattern storages are compatible""" return self.pattern_storage.is_compatible( patternset.pattern_storage) and \ self.pattern_storage.is_compatible( patternset.derived_pattern_storage) and \ self.derived_pattern_storage.is_compatible( patternset.derived_pattern_storage) and \ self.derived_pattern_storage.is_compatible( patternset.pattern_storage) def get_incompatible_pattern(self, patternset): """Return a set of incompatible pattern names between the current and the given parameterset""" result = set() result.update(self.pattern_storage.get_incompatible_parameter( patternset.pattern_storage)) result.update(self.pattern_storage.get_incompatible_parameter( patternset.derived_pattern_storage)) result.update(self.derived_pattern_storage.get_incompatible_parameter( patternset.pattern_storage)) result.update(self.derived_pattern_storage.get_incompatible_parameter( patternset.derived_pattern_storage)) return result def __repr__(self): return "Patternset: pattern:{0} derived pattern:{1}".format( dict([[pattern.name, pattern.value] for pattern in self._pattern]), dict([[pattern.name, pattern.value] for pattern in self._derived_pattern])) def __contains__(self, pattern): if isinstance(pattern, Pattern): if pattern.name in self._pattern: return pattern.is_equivalent( self._pattern[pattern.name]) elif pattern.name in self._derived_pattern: return pattern.is_equivalent( self._derived_pattern[pattern.name]) else: return False else: return (pattern in self._pattern) or \ (pattern in self._derived_pattern) def __getitem__(self, name): """Returns pattern given by name. Is pattern not found, None will be returned""" if name in self._pattern: return self._pattern[name] elif name in self._derived_pattern: return self._derived_pattern[name] else: return None class Pattern(jube2.parameter.StaticParameter): """A pattern can be used to scan a result file, using regular expression, or to represent a derived pattern.""" def __init__(self, name, value, pattern_mode="pattern", content_type="string", unit="", default=None, dotall=False): self._derived = pattern_mode != "pattern" if not self._derived: pattern_mode = "text" self._default = default self._dotall = dotall # Unicode conversion value = "" + value jube2.parameter.StaticParameter.__init__( self, name, value, parameter_type=content_type, parameter_mode=pattern_mode) self._unit = unit @property def derived(self): """pattern is a derived pattern""" return self._derived @property def content_type(self): """Return pattern type""" return self._type @property def default_value(self): """Return pattern default value""" return self._default @property def dotall(self): """Return pattern dot regex handling""" return self._dotall @property def unit(self): """Return unit""" return self._unit def substitute_and_evaluate(self, parametersets=None, final_sub=False, no_templates=True, force_evaluation=False): """Substitute all variables inside the pattern value by using the parameter inside the given parameterset and additional_parameterset. final_sub marks the last substitution. Return the new pattern and a boolean value which represent a change of value """ try: # To take care of default values for derived pattern sets, always # run final_sub instead of force_evaluation. Otherwise no error # will be thrown. Only using the final_sub setup is too late # because the default pattern might be used within another derived # pattern if (self._mode in jube2.conf.ALLOWED_SCRIPTTYPES and force_evaluation and self._default is not None): final_sub = True force_evaluation = False param, changed = \ jube2.parameter.StaticParameter.substitute_and_evaluate( self, parametersets, final_sub, no_templates, force_evaluation) except RuntimeError as re: LOGGER.debug(str(re).replace("parameter", "pattern")) if self._default is not None: value = self._default elif self._type in ["int", "float"]: value = "nan" else: value = "" pattern = Pattern( self._name, value, "text", self._type, self._unit, dotall=self._dotall) pattern.based_on = self return pattern, True if changed: # Convert parameter to pattern if not self.derived: pattern_mode = "pattern" else: pattern_mode = param.mode pattern = Pattern(param.name, param.value, pattern_mode, param.parameter_type, self._unit, dotall=self._dotall) pattern.based_on = param.based_on else: pattern = param return pattern, changed def etree_repr(self, use_current_selection=False): """Return etree object representation""" pattern_etree = ET.Element('pattern') pattern_etree.attrib["name"] = self._name pattern_etree.attrib["type"] = self._type pattern_etree.attrib["dotall"] = str(self._dotall) if self._default is not None: pattern_etree.attrib["default"] = self._default if not self._derived: pattern_etree.attrib["mode"] = "pattern" else: pattern_etree.attrib["mode"] = self._mode if self._unit != "": pattern_etree.attrib["unit"] = self._unit pattern_etree.text = self.value return pattern_etree def __repr__(self): return "Pattern({0})".format(self.__dict__) def get_jube_pattern(): """Return jube internal patternset""" patternset = Patternset() # Pattern for integer number patternset.add_pattern(Pattern("jube_pat_int", r"([+-]?\d+)")) # Pattern for integer number, no () patternset.add_pattern(Pattern("jube_pat_nint", r"(?:[+-]?\d+)")) # Pattern for floating point number patternset.add_pattern( Pattern("jube_pat_fp", r"([+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))")) # Pattern for floating point number, no () patternset.add_pattern( Pattern("jube_pat_nfp", r"(?:[+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))")) # Pattern for word (all noblank characters) patternset.add_pattern(Pattern("jube_pat_wrd", r"(\S+)")) # Pattern for word (all noblank characters), no () patternset.add_pattern(Pattern("jube_pat_nwrd", r"(?:\S+)")) # Pattern for blank space (variable length) patternset.add_pattern(Pattern("jube_pat_bl", r"(?:\s+)")) return patternset ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result.py0000644000175000017500000002201100000000000016342 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Resulttype definition""" from __future__ import (print_function, unicode_literals, division) import jube2.util.util import xml.etree.ElementTree as ET import re import jube2.log LOGGER = jube2.log.get_logger(__name__) class Result(object): """A generic result type""" class ResultData(object): """A gerneric result data type""" def __init__(self, name): self._name = name @property def name(self): """Return the result name""" return self._name def create_result(self, show=True, filename=None, **kwargs): """Create result output""" raise NotImplementedError("") def add_result_data(self, result_data): """Add additional result data""" raise NotImplementedError("") def __eq__(self, other): return self.name == other.name def __init__(self, name, res_filter=None): self._use = set() self._name = name self._res_filter = res_filter self._result_dir = None self._benchmark = None @property def name(self): """Return the result name""" return self._name @property def benchmark(self): """Return the benchmark""" return self._benchmark @property def result_dir(self): """Return the result_dir""" return self._result_dir @result_dir.setter def result_dir(self, result_dir): """Set the result_dir""" self._result_dir = result_dir @benchmark.setter def benchmark(self, benchmark): """Set the benchmark""" self._benchmark = benchmark def add_uses(self, use_names): """Add an addtional analyser name""" for use_name in use_names: if use_name in self._use: raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.add(use_name) def create_result_data(self): """Create result representation""" raise NotImplementedError("") def _analyse_data(self): """Load analyse data out of given analysers""" for analyser_name in self._use: analyser = self._benchmark.analyser[analyser_name] analyse = analyser.analyse_result # Ignore empty analyse results if analyse is None: LOGGER.debug(("No data found for analyser \"{0}\" " "in benchmark run {1}. " "Run analyse step automatically.") .format(analyser_name, self._benchmark.id)) self._benchmark.analyse(show_info=False, specific_analyser_name=analyser_name) analyse = \ self._benchmark.analyser[analyser_name].analyse_result # Check if analyse is still empty if analyse is None: LOGGER.warning(("No data found for analyser \"{0}\" " "in benchmark run {1}.") .format(analyser_name, self._benchmark.id)) continue # Create workpackage chains wp_chains = list() all_wps = set() for ids in [analyse[stepname].keys() for stepname in analyse]: all_wps.update(set(map(int, ids))) # Find workpackages without children (or at least no childen in # the given analyser) last_wps = set() for id in all_wps: child_ids = set([wp.id for wp in self._benchmark. workpackage_by_id(id).children_future]) if not child_ids.intersection(all_wps): last_wps.add(id) while (len(last_wps) > 0): next_id = last_wps.pop() # Create new chain wp_chains.append(list()) # Add all parents to the chain for wp in self._benchmark.workpackage_by_id(next_id).\ parent_history: if wp.id not in wp_chains[-1]: wp_chains[-1].append(wp.id) # Add wp itself to the chain wp_chains[-1].append(next_id) # Create output datasets by combining analyse and parameter data for chain in wp_chains: analyse_dict = dict() for wp_id in chain: workpackage = self._benchmark.workpackage_by_id(wp_id) # add analyse data if (wp_id in all_wps): analyse_dict.update( analyse[workpackage.step.name][wp_id]) # add parameter parameter_dict = dict() for par in workpackage.parameterset: value = \ jube2.util.util.convert_type(par.parameter_type, par.value, stop=False) # add suffix to the parameter name if (par.name + "_" + workpackage.step.name not in parameter_dict): parameter_dict[par.name + "_" + workpackage.step.name] = value # parmater without suffix is used for the last WP in # the chain if wp_id == chain[-1]: parameter_dict[par.name] = value analyse_dict.update(parameter_dict) # Add jube additional information analyse_dict.update({ "jube_res_analyser": analyser_name, }) # If res_filter is set, only show matching result lines if self._res_filter is not None: res_filter = jube2.util.util.substitution( self._res_filter, analyse_dict) if not jube2.util.util.eval_bool(res_filter): continue yield analyse_dict def _load_units(self, pattern_names): """Load units""" units = dict() alt_pattern_names = list(pattern_names) for i, pattern_name in enumerate(alt_pattern_names): for option in ["first", "last", "min", "max", "avg", "sum", "std"]: matcher = re.match("^(.+)_{0}$".format(option), pattern_name) if matcher: alt_pattern_names[i] = matcher.group(1) for analyser_name in self._use: if analyser_name not in self._benchmark.analyser: raise RuntimeError( " not found".format(analyser_name)) patternset_names = \ self._benchmark.analyser[analyser_name].use.copy() for analyse_files in \ self._benchmark.analyser[analyser_name].analyser.values(): for analyse_file in analyse_files: for use in analyse_file.use: patternset_names.add(use) for patternset_name in patternset_names: patternset = self._benchmark.patternsets[patternset_name] for i, pattern_name in enumerate(pattern_names): alt_pattern_name = alt_pattern_names[i] if (pattern_name in patternset) or \ (alt_pattern_name in patternset): pattern = patternset[pattern_name] if pattern is None: pattern = patternset[alt_pattern_name] if (pattern.unit is not None) and (pattern.unit != ""): units[pattern_name] = pattern.unit 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.8943863 JUBE-2.5.1/jube2/result_types/0000755000175000017500000000000000000000000017220 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/__init__.py0000644000175000017500000000145200000000000021333 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2.result_types package""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/database.py0000644000175000017500000001672700000000000021353 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Databasetype definition""" from __future__ import (print_function, unicode_literals, division) import sqlite3 import ast import os from jube2.result_types.genericresult import GenericResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log LOGGER = jube2.log.get_logger(__name__) class Database(GenericResult): """A database result""" class DatabaseData(GenericResult.KeyValuesData): """Database data""" def __init__(self, name_or_other, primekeys, db_file): if type(name_or_other) is GenericResult.KeyValuesData: self._name = name_or_other.name #self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: GenericResult.KeyValuesData.__init__(self, name_or_other) self._primekeys = primekeys self._db_file = None if db_file == "None" else db_file def create_result(self, show=True, filename=None, **kwargs): # Place for the magic # # show = If False do not show something on screen (result # only into file) # filename = name of standard output/datbase file # All keys: print([key.name for key in self._keys]) #col_names = [key.name for key in self._keys] # All data: print(self.data) keys = [k.name for k in self._data.keys()] # check if all primekeys are in keys if not set(self._primekeys).issubset(set(keys)): raise ValueError("primekeys are not included in !") # define database file if self._db_file is not None and filename is not None: file_handle = open(filename, "w") file_handle.write(self._db_file) file_handle.close() # create directory path to db file, if it does not exist file_path_ind = self._db_file.rfind('/') if file_path_ind != -1: # modify when Python2.7 support is dropped (potential race condition) if not os.path.exists(os.path.expanduser(self._db_file[:file_path_ind])): os.makedirs(os.path.expanduser( self._db_file[:file_path_ind])) db_file = os.path.expanduser(self._db_file) elif filename is not None: db_file = filename else: return None # create database and insert the data con = sqlite3.connect(db_file) cur = con.cursor() # create a string of keys and their data type to create the database table key_dtypes = {k.name: type(v[0]).__name__.replace( 'str', 'text') for (k, v) in self._data.items()} db_col_insert_types = str(key_dtypes).replace( '{', '(').replace('}', ')').replace("'", '').replace(':', '') if len(self._primekeys) > 0: db_col_insert_types = db_col_insert_types[:-1] + \ ", PRIMARY KEY {})".format(tuple(self._primekeys)) # create new table with a name of stored in variable self.name if it does not exists LOGGER.debug("CREATE TABLE IF NOT EXISTS {} {};".format( self.name, db_col_insert_types)) cur.execute("CREATE TABLE IF NOT EXISTS {} {};".format( self.name, db_col_insert_types)) # check for primary keys in database table cur.execute('PRAGMA TABLE_INFO({})'.format(self.name)) db_primary_keys = [i[1] for i in cur.fetchall() if i[5] != 0] if not set(self._primekeys) == set(db_primary_keys): raise ValueError("Modification of primary values is not supported. " + "Primary keys of table {} are {}".format(self.name, db_primary_keys)) # compare self._keys with columns in db and add new column in the database if it does not exist cur.execute("SELECT * FROM {}".format(self.name)) db_col_names = [tup[0] for tup in cur.description] # delete columns, which were removed as keys in this execution diff_col_list = list(set(db_col_names).difference(keys)) if len(diff_col_list) != 0: for col in diff_col_list: LOGGER.debug( "ALTER TABLE {} DROP COLUMN {}".format(self.name, col)) cur.execute( "ALTER TABLE {} DROP COLUMN {}".format(self.name, col)) # add columns, which were added as keys in this execution diff_col_list = list(set(keys).difference(db_col_names)) if len(diff_col_list) != 0: for col in diff_col_list: LOGGER.debug("ALTER TABLE {} ADD COLUMN {} {}".format( self.name, col, type(col).__name__.replace('str', 'text'))) cur.execute("ALTER TABLE {} ADD COLUMN {} {}".format( self.name, col, type(col).__name__.replace('str', 'text'))) # insert or replace self.data in database replace_query = "REPLACE INTO {} {} VALUES (".format( self.name, tuple(keys)) + "{}".format('?,'*len(keys))[:-1] + ");" LOGGER.debug(replace_query) cur.executemany( replace_query, [d for d in list(zip(*self._data.values()))]) con.commit() con.close() # Print database location to screen and result.log LOGGER.info("Database location of id {}: {}".format( self._benchmark_ids[0], db_file)) def __init__(self, name, res_filter=None, primekeys=None, db_file=None): GenericResult.__init__(self, name, res_filter) self._primekeys = primekeys self._db_file = db_file def create_result_data(self, style=None): """Create result data""" result_data = GenericResult.create_result_data(self) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/genericresult.py0000644000175000017500000001455400000000000022456 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """GernicResultType definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result import Result import jube2.log import xml.etree.ElementTree as ET import operator import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class GenericResult(Result): """A generic result type""" class KeyValuesData(Result.ResultData): """Generic key value data""" def __init__(self, other_or_name): if type(other_or_name) is str: Result.ResultData.__init__(self, other_or_name) elif type(other_or_name) is Result.ResultData: self._name = other_or_name.name self._data = dict() self._benchmark_ids = list() @property def keys(self): """Return keys""" return self._data.keys() @property def data(self): """Return data""" return self._data @property def data_dict(self): """Return unordered dictionary representation of data""" return self._data @property def benchmark_ids(self): """Return benchmark ids""" return self._benchmark_ids def add_key_value_data(self, data, benchmark_ids): """Add a list of additional rows to current result data""" # Add new keys to for old rows for key in data.keys(): if key not in self._data.keys(): if len(self._benchmark_ids) > 0: self._data[key] = [None] * len(self._benchmark_ids) else: self._data[key] = list() number_of_new_values = len(list(data.values())[0]) # Add new rows for key in self._data.keys(): if key in data.keys(): self._data[key] += data[key] else: self._data[key] += [None] * number_of_new_values if type(benchmark_ids) is int: self._benchmark_ids += [benchmark_ids] * number_of_new_values if type(benchmark_ids) is list: self._benchmark_ids += benchmark_ids def add_result_data(self, result_data): """Add additional result data""" if self.name != result_data.name: raise RuntimeError("Cannot combine to different result sets.") self.add_key_value_data(result_data.data, result_data.benchmark_ids) def create_result(self, show=True, filename=None, **kwargs): """Create result representation""" raise NotImplementedError("") class DataKey(object): """Class represents one data key """ def __init__(self, name, title=None, unit=None): self._name = name self._title = title self._unit = unit @property def title(self): """Key title""" return self._title @property def name(self): """Key name""" return self._name @property def unit(self): """Key data unit""" return self._unit @unit.setter def unit(self, unit): """Set key data unit""" self._unit = unit @property def resulting_name(self): """Column name based on name, title and unit""" if self._title is not None: name = self._title else: name = self._name if self._unit is not None: name += "[{0}]".format(self._unit) return name def etree_repr(self): """Return etree object representation""" key_etree = ET.Element("key") key_etree.text = self._name if self._title is not None: key_etree.attrib["title"] = self._title return key_etree def __eq__(self, other): return self.resulting_name == other.resulting_name def __hash__(self): return hash(self.resulting_name) def __init__(self, name, res_filter=None): Result.__init__(self, name, res_filter) self._keys = list() def add_key(self, name, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(GenericResult.DataKey(name, title, unit)) def create_result_data(self): """Create result data""" result_data = GenericResult.KeyValuesData(self._name) # 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] # 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/keyvaluesresult.py0000644000175000017500000002256600000000000023054 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """KeyValuesResulttype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result import Result import jube2.log import xml.etree.ElementTree as ET import operator import jube2.util.util import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class KeyValuesResult(Result): """A generic key value result type""" class KeyValuesData(Result.ResultData): """Key value data""" def __init__(self, other_or_name): if type(other_or_name) is str: Result.ResultData.__init__(self, other_or_name) elif type(other_or_name) is Result.ResultData: self._name = other_or_name.name self._data = list() self._keys = list() self._benchmark_ids = list() @property def keys(self): """Return keys""" return self._keys @property def data(self): """Return table data""" return self._data @property def data_dict(self): """Return unordered dictionary representation of data""" result_dict = dict() for i, key in enumerate(self._keys): result_dict[key] = list() for data in self._data: result_dict[key].append(data[i]) return result_dict @property def benchmark_ids(self): """Return benchmark ids""" return self._benchmark_ids def add_key_value_data(self, keys, data, benchmark_ids): """Add a list of additional rows to current result data""" order = list() last_index = len(self._keys) # Find matching rows for key in keys: if key in self._keys: index = self._keys.index(key) # Check weather key occurs multiple times while index in order: try: index = self._keys.index(key, index + 1) except ValueError: index = len(self._keys) self._keys.append(key) else: index = len(self._keys) self._keys.append(key) order.append(index) # Fill up existing rows if last_index != len(self._keys): for row in self._data: row += ["" for key in self._keys[last_index:]] # Add new rows for row in data: new_row = ["" for key in self._keys] for i, index in enumerate(order): new_row[index] = row[i] self._data.append(new_row) if type(benchmark_ids) is int: self._benchmark_ids.append(benchmark_ids) if type(benchmark_ids) is list: self._benchmark_ids += benchmark_ids def add_id_information(self, reverse=False): """Add additional id key to table data.""" id_key = KeyValuesResult.DataKey("id") if id_key not in self._keys: # Add key at the beginning of keys list self._keys.insert(0, id_key) for i, data in enumerate(self._data): data.insert(0, self._benchmark_ids[i]) # Sort data by using new id key (stable sort) self._data.sort(key=operator.itemgetter(0), reverse=reverse) for i, data in enumerate(self._data): self._data[i][0] = str(data[0]) def add_result_data(self, result_data): """Add additional result data""" if self.name != result_data.name: raise RuntimeError("Cannot combine to different result sets.") self.add_key_value_data(result_data.keys, result_data.data, result_data.benchmark_ids) def create_result(self, show=True, filename=None, **kwargs): """Create result representation""" raise NotImplementedError("") class DataKey(object): """Class represents one data key """ def __init__(self, name, title=None, format_string=None, unit=None): self._name = name self._title = title self._format_string = format_string self._unit = unit @property def title(self): """Key title""" return self._title @property def name(self): """Key name""" return self._name @property def format(self): """Key data format""" return self._format_string @property def unit(self): """Key data unit""" return self._unit @unit.setter def unit(self, unit): """Set key data unit""" self._unit = unit @property def resulting_name(self): """Column name based on name, title and unit""" if self._title is not None: name = self._title else: name = self._name if self._unit is not None: name += "[{0}]".format(self._unit) return name def etree_repr(self): """Return etree object representation""" key_etree = ET.Element("key") key_etree.text = self._name if self._format_string is not None: key_etree.attrib["format"] = self._format_string if self._title is not None: key_etree.attrib["title"] = self._title return key_etree def __eq__(self, other): return self.resulting_name == other.resulting_name def __hash__(self): return hash(self.resulting_name) def __init__(self, name, sort_names=None, res_filter=None): Result.__init__(self, name, res_filter) self._keys = list() if sort_names is None: self._sort_names = list() else: self._sort_names = sort_names def add_key(self, name, format_string=None, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(KeyValuesResult.DataKey(name, title, format_string, unit)) def create_result_data(self): """Create result data""" result_data = KeyValuesResult.KeyValuesData(self._name) # Read pattern/parameter units if available units = self._load_units([key.name for key in self._keys]) for key in self._keys: if key.name in units: key.unit = units[key.name] sort_data = list() for dataset in self._analyse_data(): # Add additional data if needed for sort_name in self._sort_names: if sort_name not in dataset: dataset[sort_name] = None sort_data.append(dataset) # Sort the resultset if len(self._sort_names) > 0: LOGGER.debug("sort using: {0}".format(",".join(self._sort_names))) # Use CompType for sorting to allow comparison of None values sort_data = \ sorted(sort_data, key=lambda x: [jube2.util.util.CompType(x[sort_name]) for sort_name in self._sort_names]) # Create table data table_data = list() for dataset in sort_data: row = list() cnt = 0 for key in self._keys: if key.name in dataset: # Cnt number of final entries to avoid complete empty # result entries cnt += 1 # Set null value if dataset[key.name] is None: value = "" else: # Format data values to create string representation if key.format is not None: value = jube2.util.output.format_value( key.format, dataset[key.name]) else: value = str(dataset[key.name]) row.append(value) else: row.append(None) if cnt > 0: table_data.append(row) # Add data to toe result set result_data.add_key_value_data(self._keys, table_data, self._benchmark.id) return result_data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/syslog.py0000644000175000017500000001354600000000000021123 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Syslogtype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result_types.keyvaluesresult import KeyValuesResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log import jube2.conf import logging.handlers LOGGER = jube2.log.get_logger(__name__) class SysloggedResult(KeyValuesResult): """A result that gets sent to syslog.""" class SyslogData(KeyValuesResult.KeyValuesData): """Syslog data""" def __init__(self, name_or_other, syslog_address=None, syslog_host=None, syslog_port=None, syslog_fmt_string=None): if type(name_or_other) is KeyValuesResult.KeyValuesData: self._name = name_or_other.name self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: KeyValuesResult.KeyValuesData.__init__(self, name_or_other) self._syslog_address = syslog_address self._syslog_host = syslog_host self._syslog_port = syslog_port self._syslog_fmt_string = syslog_fmt_string def create_result(self, show=True, filename=None, **kwargs): """Create result output""" # If there are multiple benchmarks, add benchmark id information if len(set(self._benchmark_ids)) > 1: self.add_id_information(reverse=kwargs.get("reverse", False)) if self._syslog_address is not None: address = self._syslog_address else: address = (self._syslog_host, self._syslog_port) handler = logging.handlers.SysLogHandler( address=address, facility=logging.handlers.SysLogHandler.LOG_USER ) handler.setFormatter(logging.Formatter( fmt=self._syslog_fmt_string)) # get logger log = logging.getLogger("jube") log.setLevel(logging.INFO) log.addHandler(handler) # create log output for dataset in self.data: entry = list() for i, key in enumerate(self.keys): entry.append("{0}={1}".format(key.name, dataset[i])) # Log result if show: if not jube2.conf.DEBUG_MODE: log.info(" ".join(entry)) LOGGER.debug("Logged: {0}\n".format(" ".join(entry))) # remove handler to avoid double logging log.removeHandler(handler) def __init__(self, name, syslog_address=None, syslog_host=None, syslog_port=None, syslog_fmt_string=None, sort_names=None, res_filter=None): KeyValuesResult.__init__(self, name, sort_names, res_filter) if (syslog_address is None) and (syslog_host is None) and \ (syslog_port is None): raise IOError("Neither a syslog address nor a hostname port " + "combination specified.") if (syslog_host is not None) and (syslog_address is not None): raise IOError("Please specify a syslog address or a hostname, " + "not both at the same time.") if (syslog_host is not None) and (syslog_port is None): self._syslog_port = 514 self._syslog_address = syslog_address self._syslog_host = syslog_host self._syslog_port = syslog_port if syslog_fmt_string is None: self._syslog_fmt_string = jube2.conf.SYSLOG_FMT_STRING else: self._syslog_fmt_string = syslog_fmt_string def create_result_data(self, style=None): """Create result data""" result_data = KeyValuesResult.create_result_data(self) return SysloggedResult.SyslogData(result_data, self._syslog_address, self._syslog_host, self._syslog_port, self._syslog_fmt_string) def etree_repr(self): """Return etree object representation""" result_etree = Result.etree_repr(self) syslog_etree = ET.SubElement(result_etree, "syslog") syslog_etree.attrib["name"] = self._name if self._syslog_address is not None: syslog_etree.attrib["address"] = self._syslog_address if self._syslog_host is not None: syslog_etree.attrib["host"] = self._syslog_host if self._syslog_port is not None: syslog_etree.attrib["port"] = self._syslog_port if self._syslog_fmt_string is not None: syslog_etree.attrib["format"] = self._syslog_fmt_string if self._res_filter is not None: syslog_etree.attrib["filter"] = self._res_filter if len(self._sort_names) > 0: syslog_etree.attrib["sort"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._sort_names) for key in self._keys: syslog_etree.append(key.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/result_types/table.py0000644000175000017500000001571200000000000020667 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Tabletype definition""" from __future__ import (print_function, unicode_literals, division) from jube2.result_types.keyvaluesresult import KeyValuesResult from jube2.result import Result import xml.etree.ElementTree as ET import jube2.log import jube2.util.output LOGGER = jube2.log.get_logger(__name__) class Table(KeyValuesResult): """A ascii based result table""" class TableData(KeyValuesResult.KeyValuesData): """Table data""" def __init__(self, name_or_other, style, separator, transpose): if type(name_or_other) is KeyValuesResult.KeyValuesData: self._name = name_or_other.name self._keys = name_or_other.keys self._data = name_or_other.data self._benchmark_ids = name_or_other.benchmark_ids else: KeyValuesResult.KeyValuesData.__init__(self, name_or_other) self._style = style self._separator = separator # Ignore separator if pretty style is used if self._style == "pretty": self._separator = None elif self._separator is None: self._separator = jube2.conf.DEFAULT_SEPARATOR self._transpose = transpose @property def _columns(self): """Get columns""" return self._keys @property def style(self): """Get style""" return self._style @style.setter def style(self, style): """Set style""" self._style = style @property def separator(self): """Get separator""" return self._separator @separator.setter def separator(self, separator): """Set separator""" self._separator = separator def __str__(self): colw = list() for column in self._columns: if type(column) is Table.Column: if column.colw is None: colw.append(0) else: colw.append(column.colw) else: colw.append(0) data = list() data.append([column.resulting_name for column in self._columns]) data += self._data data = [['' if c is None else c for c in r] for r in data] if self._style == "pretty": output = "{0}:\n".format(self.name) else: output = "" output += jube2.util.output.text_table( data, use_header_line=True, auto_linebreak=False, colw=colw, indent=0, style=self._style, separator=self._separator, transpose=self._transpose) return output def create_result(self, show=True, filename=None, **kwargs): """Create result output""" # If there are multiple benchmarks, add benchmark id information if len(set(self._benchmark_ids)) > 1: self.add_id_information(reverse=kwargs.get("reverse", False)) result_str = str(self) # Print result to screen if show: LOGGER.info(result_str) LOGGER.info("\n") else: LOGGER.debug(result_str) LOGGER.debug("\n") # Print result to file if filename is not None: file_handle = open(filename, "w") file_handle.write(result_str) file_handle.close() class Column(KeyValuesResult.DataKey): """Class represents one table column""" def __init__(self, name, title=None, colw=None, format_string=None, unit=None): KeyValuesResult.DataKey.__init__(self, name, title, format_string, unit) self._colw = colw @property def colw(self): """Column width""" return self._colw def etree_repr(self): """Return etree object representation""" column_etree = KeyValuesResult.DataKey.etree_repr(self) column_etree.tag = "column" if self._colw is not None: column_etree.attrib["colw"] = str(self._colw) return column_etree def __init__(self, name, style="csv", separator=jube2.conf.DEFAULT_SEPARATOR, sort_names=None, transpose=False, res_filter=None): KeyValuesResult.__init__(self, name, sort_names, res_filter) self._style = style self._separator = separator self._transpose = transpose def add_column(self, name, colw=None, format_string=None, title=None): """Add an additional column to the dataset""" self._keys.append(Table.Column(name, title, colw, format_string)) def add_key(self, name, format_string=None, title=None, unit=None): """Add an additional key to the dataset""" self._keys.append(Table.Column(name, title, None, format_string)) def create_result_data(self, style): """Create result data""" result_data = KeyValuesResult.create_result_data(self) return Table.TableData(result_data, style if style is not None else self._style, self._separator, self._transpose) def etree_repr(self): """Return etree object representation""" result_etree = Result.etree_repr(self) table_etree = ET.SubElement(result_etree, "table") table_etree.attrib["name"] = self._name table_etree.attrib["style"] = self._style if self._separator is not None: table_etree.attrib["separator"] = self._separator if self._res_filter is not None: table_etree.attrib["filter"] = self._res_filter table_etree.attrib["transpose"] = str(self._transpose) if len(self._sort_names) > 0: table_etree.attrib["sort"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._sort_names) for column in self._keys: table_etree.append(column.etree_repr()) return result_etree ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/step.py0000644000175000017500000010173200000000000016007 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Step contains the commands for steps""" from __future__ import (print_function, unicode_literals, division) import subprocess import os import re import time import xml.etree.ElementTree as ET import jube2.util.util import jube2.conf import jube2.log import jube2.parameter LOGGER = jube2.log.get_logger(__name__) class Step(object): """A Step represent one execution step. It contains a list of Do-operations and multiple parametersets, substitutionsets and filesets. A Step is a template for Workpackages. """ def __init__(self, name, depend, iterations=1, alt_work_dir=None, shared_name=None, export=False, max_wps="0", active="true", suffix="", cycles=1, procs=1, do_log_file=None): self._name = name self._use = list() self._operations = list() self._iterations = iterations self._depend = depend self._alt_work_dir = alt_work_dir self._shared_name = shared_name self._export = export self._max_wps = max_wps self._active = active self._suffix = suffix self._cycles = cycles self._procs = procs self._do_log_file = do_log_file def etree_repr(self): """Return etree object representation""" step_etree = ET.Element("step") step_etree.attrib["name"] = self._name if len(self._depend) > 0: step_etree.attrib["depend"] = \ jube2.conf.DEFAULT_SEPARATOR.join(self._depend) if self._alt_work_dir is not None: step_etree.attrib["work_dir"] = self._alt_work_dir if self._shared_name is not None: step_etree.attrib["shared"] = self._shared_name if self._active != "true": step_etree.attrib["active"] = self._active if self._suffix != "": step_etree.attrib["suffix"] = self._suffix if self._export: step_etree.attrib["export"] = "true" if self._max_wps != "0": step_etree.attrib["max_async"] = self._max_wps if self._iterations > 1: step_etree.attrib["iterations"] = str(self._iterations) if self._cycles > 1: step_etree.attrib["cycles"] = str(self._cycles) if self._procs != 1: step_etree.attrib["procs"] = str(self._procs) if self._do_log_file != None: step_etree.attrib["do_log_file"] = str(self._do_log_file) for use in self._use: use_etree = ET.SubElement(step_etree, "use") use_etree.text = jube2.conf.DEFAULT_SEPARATOR.join(use) for operation in self._operations: step_etree.append(operation.etree_repr()) return step_etree def __repr__(self): return "{0}".format(vars(self)) def add_operation(self, operation): """Add operation""" self._operations.append(operation) def add_uses(self, use_names): """Add use""" for use_name in use_names: if any([use_name in use_list for use_list in self._use]): raise ValueError(("Element \"{0}\" can only be used once") .format(use_name)) self._use.append(use_names) @property def name(self): """Return step name""" return self._name @property def active(self): """Return active state""" return self._active @property def export(self): """Return export behaviour""" return self._export @property def iterations(self): """Return iterations""" return self._iterations @property def cycles(self): """Return number of cycles""" return self._cycles @property def procs(self): """Return number of procs""" return self._procs @property def shared_link_name(self): """Return shared link name""" return self._shared_name @property def max_wps(self): """Return maximum number of simultaneous workpackages""" return self._max_wps @property def do_log_file(self): """Return do log file name""" return self._do_log_file def get_used_sets(self, available_sets, parameter_dict=None): """Get list of all used sets, which can be found in available_sets""" set_names = list() if parameter_dict is None: parameter_dict = dict() for use in self._use: for name in use: name = jube2.util.util.substitution(name, parameter_dict) if (name in available_sets) and (name not in set_names): set_names.append(name) return set_names def shared_folder_path(self, benchdir, parameter_dict=None): """Return shared folder name""" if self._shared_name is not None: if parameter_dict is not None: shared_name = jube2.util.util.substitution(self._shared_name, parameter_dict) else: shared_name = self._shared_name return os.path.join(benchdir, "{0}_{1}".format(self._name, shared_name)) else: return "" def get_jube_parameterset(self): """Return parameterset which contains step related information""" parameterset = jube2.parameter.Parameterset() # step name parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_name", self._name, update_mode=jube2.parameter.JUBE_MODE)) # iterations parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_iterations", str(self._iterations), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # cycles parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_step_cycles", str(self._cycles), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # default worpackage cycle, will be overwritten by specific worpackage # cycle parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_cycle", "0", parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_workpackages(self, benchmark, global_parameterset, local_parameterset=None, used_sets=None, iteration_base=0, parents=None, incompatible_parameters=None): """Create workpackages for current step using given benchmark context""" if used_sets is None: used_sets = set() update_parameters = jube2.parameter.Parameterset() if local_parameterset is None: local_parameterset = jube2.parameter.Parameterset() global_parameterset.add_parameterset( benchmark.get_jube_parameterset()) global_parameterset.add_parameterset(self.get_jube_parameterset()) update_parameters.add_parameterset( global_parameterset.get_updatable_parameter( jube2.parameter.STEP_MODE)) for parameter in update_parameters: incompatible_parameters.discard(parameter.name) if parents is None: parents = list() new_workpackages = list() # Create parameter dictionary for substitution parameter_dict = \ dict([[par.name, par.value] for par in global_parameterset.constant_parameter_dict.values()]) # Filter for parametersets in uses parameterset_names = \ set(self.get_used_sets(benchmark.parametersets, parameter_dict)) new_sets_found = len(parameterset_names.difference(used_sets)) > 0 if new_sets_found: parameterset_names = parameterset_names.difference(used_sets) used_sets = used_sets.union(parameterset_names) for parameterset_name in parameterset_names: # The parametersets in a single step must be compatible if not local_parameterset.is_compatible( benchmark.parametersets[parameterset_name]): incompatible_names = \ local_parameterset.get_incompatible_parameter( benchmark.parametersets[parameterset_name]) raise ValueError(("Cannot use parameterset '{0}' in " + "step '{1}'.\nParameter '{2}' is/are " + "already defined by a different " + "parameterset.") .format(parameterset_name, self.name, ",".join(incompatible_names))) local_parameterset.add_parameterset( benchmark.parametersets[parameterset_name]) # Combine local and history parameterset if local_parameterset.is_compatible( global_parameterset, update_mode=jube2.parameter.USE_MODE): update_parameters.add_parameterset( local_parameterset.get_updatable_parameter( jube2.parameter.USE_MODE)) for parameter in update_parameters: incompatible_parameters.discard(parameter.name) global_parameterset = \ local_parameterset.copy().add_parameterset( global_parameterset) else: incompatible_names = \ local_parameterset.get_incompatible_parameter( global_parameterset, update_mode=jube2.parameter.USE_MODE) LOGGER.debug("Incompatible parameterset combination found " + "between current and parent steps. \nParameter " + "'{0}' is/are already defined different.".format( ",".join(incompatible_names))) return new_workpackages # update parameters global_parameterset.update_parameterset(update_parameters) # Set tag-mode evaluation helper function to allow access to tag list # during paramter evaluation for parameter in global_parameterset.all_parameters: if parameter.mode == "tag": parameter.eval_helper = \ lambda tag: tag if tag in benchmark.tags else "" # Expand templates parametersets = [global_parameterset] change = True while change: change = False new_parametersets = list() for parameterset in parametersets: parameterset.parameter_substitution() # Maybe new templates were created if parameterset.has_templates: LOGGER.debug("Expand parameter templates:\n{0}".format( "\n".join(" \"{0}\": {1}".format(i, j.value) for i, j in parameterset. template_parameter_dict.items()))) new_parametersets += \ [new_parameterset for new_parameterset in parameterset.expand_templates()] change = True else: new_parametersets += [parameterset] parametersets = new_parametersets # Create workpackages for parameterset in parametersets: workpackage_parameterset = local_parameterset.copy() workpackage_parameterset.update_parameterset(parameterset) if new_sets_found: new_workpackages += \ self.create_workpackages(benchmark, parameterset, workpackage_parameterset, used_sets, iteration_base, parents, incompatible_parameters.copy()) else: # Check if all incompatible_parameters were updated if len(incompatible_parameters) > 0: return new_workpackages # Create new workpackage created_workpackages = list() for iteration in range(self.iterations): workpackage = jube2.workpackage.Workpackage( benchmark=benchmark, step=self, parameterset=parameterset.copy(), local_parameter_names=[ par.name for par in workpackage_parameterset], iteration=iteration_base * self.iterations + iteration, cycle=0) # --- Link parent workpackages --- for parent in parents: workpackage.add_parent(parent) # --- Add workpackage JUBE parameterset --- workpackage.parameterset.add_parameterset( workpackage.get_jube_parameterset()) # --- Final parameter substitution --- workpackage.parameterset.parameter_substitution( final_sub=True) # --- Check parameter type --- for parameter in workpackage.parameterset: if not parameter.is_template: jube2.util.util.convert_type( parameter.parameter_type, parameter.value) # --- Enable workpackage dir cache --- workpackage.allow_workpackage_dir_caching() if workpackage.active: created_workpackages.append(workpackage) else: jube2.workpackage.Workpackage.\ reduce_workpackage_id_counter() for workpackage in created_workpackages: workpackage.iteration_siblings.update( set(created_workpackages)) new_workpackages += created_workpackages return new_workpackages @property def alt_work_dir(self): """Return alternativ work directory""" return self._alt_work_dir @property def use(self): """Return parameters and substitutions""" return self._use @property def suffix(self): """Return directory suffix""" return self._suffix @property def operations(self): """Return operations""" return self._operations @property def depend(self): """Return dependencies""" return self._depend def get_depend_history(self, benchmark): """Creates a set of all dependent steps in history for given benchmark""" depend_history = set() for step_name in self._depend: if step_name not in depend_history: depend_history.add(step_name) depend_history.update( benchmark.steps[step_name].get_depend_history(benchmark)) return depend_history class Operation(object): """The Operation-class represents a single instruction, which will be executed in a shell environment. """ def __init__(self, do, async_filename=None, stdout_filename=None, stderr_filename=None, active="true", shared=False, work_dir=None, break_filename=None, error_filename=None): self._do = do self._error_filename = error_filename self._async_filename = async_filename self._break_filename = break_filename self._stdout_filename = stdout_filename self._stderr_filename = stderr_filename self._active = active self._shared = shared self._work_dir = work_dir @property def stdout_filename(self): """Get stdout filename""" return self._stdout_filename @property def stderr_filename(self): """Get stderr filename""" return self._stderr_filename @property def error_filename(self): """Get error filename""" return self._error_filename @property def async_filename(self): """Get async filename""" return self._async_filename @property def shared(self): """Shared operation?""" return self._shared def active(self, parameter_dict): """Return active status of the current operation depending on the given parameter_dict""" active_str = jube2.util.util.substitution(self._active, parameter_dict) return jube2.util.util.eval_bool(active_str) def execute(self, parameter_dict, work_dir, only_check_pending=False, environment=None, pid=None, dolog=None): """Execute the operation. work_dir must be set to the given context path. The parameter_dict used for inline substitution. If only_check_pending is set to True, the operation will not be executed, only the async_file will be checked. Return operation status: True => operation finished False => operation pending """ if not self.active(parameter_dict): return True if environment is not None: env = environment else: env = os.environ if not only_check_pending: # Inline substitution do = jube2.util.util.substitution(self._do, parameter_dict) # Remove leading and trailing ; because otherwise ;; will cause # trouble when adding ; env do = do.strip(";") if (not jube2.conf.DEBUG_MODE) and (do.strip() != ""): # Change stdout if self._stdout_filename is not None: stdout_filename = jube2.util.util.substitution( self._stdout_filename, parameter_dict) stdout_filename = \ os.path.expandvars(os.path.expanduser(stdout_filename)) else: stdout_filename = "stdout" stdout_path = os.path.join(work_dir, stdout_filename) stdout = open(stdout_path, "a") # Change stderr if self._stderr_filename is not None: stderr_filename = jube2.util.util.substitution( self._stderr_filename, parameter_dict) stderr_filename = \ os.path.expandvars(os.path.expanduser(stderr_filename)) else: stderr_filename = "stderr" stderr_path = os.path.join(work_dir, stderr_filename) stderr = open(stderr_path, "a") # Use operation specific work directory if self._work_dir is not None and len(self._work_dir) > 0: new_work_dir = jube2.util.util.substitution( self._work_dir, parameter_dict) new_work_dir = os.path.expandvars(os.path.expanduser(new_work_dir)) work_dir = os.path.join(work_dir, new_work_dir) if re.search(jube2.parameter.Parameter.parameter_regex, work_dir): raise IOError(("Given work directory {0} contains a unknown " + "JUBE or environment variable.").format( work_dir)) # Create directory if it does not exist if not jube2.conf.DEBUG_MODE and not os.path.exists(work_dir): try: os.makedirs(work_dir) except FileExistsError: pass if not only_check_pending: if pid is not None: env_file_name = jube2.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube2.conf.ENVIRONMENT_INFO abs_info_file_path = \ os.path.abspath(os.path.join(work_dir, env_file_name)) # Select unix shell shell = jube2.conf.STANDARD_SHELL if "JUBE_EXEC_SHELL" in os.environ: alt_shell = os.environ["JUBE_EXEC_SHELL"].strip() if len(alt_shell) > 0: shell = alt_shell # Execute "do" LOGGER.debug(">>> {0}".format(do)) if (not jube2.conf.DEBUG_MODE) and (do != ""): LOGGER.debug(" stdout: {0}".format( os.path.abspath(stdout_path))) LOGGER.debug(" stderr: {0}".format( os.path.abspath(stderr_path))) try: if jube2.conf.VERBOSE_LEVEL > 1: stdout_handle = subprocess.PIPE else: stdout_handle = stdout if dolog != None: dolog.store_do(do=do, shell=shell, work_dir=os.path.abspath( work_dir), parameter_dict=parameter_dict, shared=self.shared) sub = subprocess.Popen( [shell, "-c", "{0} && env > \"{1}\"".format(do, abs_info_file_path)], cwd=work_dir, stdout=stdout_handle, stderr=stderr, shell=False, env=env) except OSError: stdout.close() stderr.close() raise RuntimeError(("Error (returncode <> 0) while " + "running \"{0}\" in " + "directory \"{1}\"") .format(do, os.path.abspath(work_dir))) # stdout verbose output if jube2.conf.VERBOSE_LEVEL > 1: while True: read_out = sub.stdout.read( jube2.conf.VERBOSE_STDOUT_READ_CHUNK_SIZE) if (not read_out): break else: try: print(read_out.decode(errors="ignore"), end="") except TypeError: print(read_out.decode("utf-8", "ignore"), end="") try: stdout.write(read_out) except TypeError: try: stdout.write(read_out.decode( errors="ignore")) except TypeError: stdout.write(read_out.decode("utf-8", "ignore")) time.sleep(jube2.conf.VERBOSE_STDOUT_POLL_SLEEP) sub.communicate() returncode = sub.wait() # Close filehandles stdout.close() stderr.close() env = Operation.read_process_environment(work_dir, pid=pid) # Read and store new environment if (environment is not None) and (returncode == 0): environment.clear() environment.update(env) if returncode != 0: if os.path.isfile(stderr_path): stderr = open(stderr_path, "r") stderr_msg = stderr.readlines() stderr.close() else: stderr_msg = "" try: raise RuntimeError( ("Error (returncode <> 0) while running \"{0}\" " + "in directory \"{1}\"\nMessage in \"{2}\":" + "{3}\n{4}").format( do, os.path.abspath(work_dir), os.path.abspath(stderr_path), "\n..." if len(stderr_msg) > jube2.conf.ERROR_MSG_LINES else "", "\n".join(stderr_msg[ -jube2.conf.ERROR_MSG_LINES:]))) except UnicodeDecodeError: raise RuntimeError( ("Error (returncode <> 0) while running \"{0}\" " + "in directory \"{1}\"").format( do, os.path.abspath(work_dir))) continue_op = True continue_cycle = True # Check if further execution was skipped if self._break_filename is not None: break_filename = jube2.util.util.substitution( self._break_filename, parameter_dict) break_filename = \ os.path.expandvars(os.path.expanduser(break_filename)) if os.path.exists(os.path.join(work_dir, break_filename)): LOGGER.debug(("\"{0}\" was found, workpackage execution and " " further loop continuation was stopped.") .format(break_filename)) continue_cycle = False # Waiting to continue if self._async_filename is not None: async_filename = jube2.util.util.substitution( self._async_filename, parameter_dict) async_filename = \ os.path.expandvars(os.path.expanduser(async_filename)) if not os.path.exists(os.path.join(work_dir, async_filename)): LOGGER.debug("Waiting for file \"{0}\" ..." .format(async_filename)) if jube2.conf.DEBUG_MODE: LOGGER.debug(" skip waiting") else: continue_op = False # Search for error file if self._error_filename is not None: error_filename = jube2.util.util.substitution( self._error_filename, parameter_dict) error_filename = \ os.path.expandvars(os.path.expanduser(error_filename)) if os.path.exists(os.path.join(work_dir, error_filename)): LOGGER.debug("Checking for error file \"{0}\" ..." .format(error_filename)) if jube2.conf.DEBUG_MODE: LOGGER.debug(" skip error") else: do = jube2.util.util.substitution(self._do, parameter_dict) raise(RuntimeError(("Error file \"{0}\" found after " + "running the command \"{1}\".").format( error_filename, do))) return continue_op, continue_cycle def etree_repr(self): """Return etree object representation""" do_etree = ET.Element("do") do_etree.text = self._do if self._async_filename is not None: do_etree.attrib["done_file"] = self._async_filename if self._error_filename is not None: do_etree.attrib["error_file"] = self._error_filename if self._break_filename is not None: do_etree.attrib["break_file"] = self._break_filename if self._stdout_filename is not None: do_etree.attrib["stdout"] = self._stdout_filename if self._stderr_filename is not None: do_etree.attrib["stderr"] = self._stderr_filename if self._active != "true": do_etree.attrib["active"] = self._active if self._shared: do_etree.attrib["shared"] = "true" if self._work_dir is not None: do_etree.attrib["work_dir"] = self._work_dir return do_etree def __repr__(self): return self._do @staticmethod def read_process_environment(work_dir, remove_after_read=True, pid=None): """Read standard environment info file in given directory.""" env = dict() last = None if pid is not None: env_file_name = jube2.conf.ENVIRONMENT_INFO.replace( '.', '_{}.'.format(pid)) else: env_file_name = jube2.conf.ENVIRONMENT_INFO env_file_path = os.path.join(work_dir, env_file_name) if os.path.isfile(env_file_path): env_file = open(env_file_path, "r") for line in env_file: line = line.rstrip() matcher = re.match(r"^(\S.*?)=(.*?)$", line) if matcher: env[matcher.group(1)] = matcher.group(2) last = matcher.group(1) elif last is not None: env[last] += "\n" + line env_file.close() if remove_after_read: os.remove(env_file_path) return env class DoLog(object): """A DoLog class containing the operations and information for setting up the do log.""" def __init__(self, log_dir, log_file, initial_env, cycle=0): self._log_dir = log_dir if log_file != None: if log_file[-1] == '/': raise ValueError( "The path of do_log_file is ending with / which is a invalid file path.") self._log_file = log_file self._initial_env = initial_env self._work_dir = None self._cycle = cycle self._log_path = None @property def log_path(self): """Get log directory""" return self._log_path @property def log_file(self): """Get log file""" return self._log_file @property def log_path(self): """Get log path""" return self._log_path @property def work_dir(self): """Get last work directory""" return self._work_dir @property def initial_env(self): """Get initial env""" return self._initial_env def initialiseFile(self, shell): """Initialise file if not yet existent.""" fdologout = open(self.log_path, 'a') fdologout.write('#!'+shell+'\n\n') for envVarName, envVarValue in self.initial_env.items(): fdologout.write('set '+envVarName+"='" + envVarValue.replace('\n', '\\n')+"'\n") fdologout.write('\n') fdologout.close() def store_do(self, do, shell, work_dir, parameter_dict=None, shared=False): """Store the current execution directive to the do log and set up the environment if file does not yet exist.""" if self._log_file == None: return if self._log_path == None: if parameter_dict: new_log_file = jube2.util.util.substitution( self._log_file, parameter_dict) new_log_file = os.path.expandvars( os.path.expanduser(new_log_file)) self._log_file = new_log_file if re.search(jube2.parameter.Parameter.parameter_regex, self._log_file): raise IOError(("Given do_log_file path {0} contains a unknown " + "JUBE or environment variable.").format( self._log_file)) if self._log_file[0] == '/': self._log_path = self._log_file elif '/' not in self._log_file: self._log_path = os.path.join(self._log_dir, self._log_file) else: self._log_path = os.path.join(os.getcwd(), self._log_file) # create directory if not yet existent if not os.path.exists(os.path.dirname(self.log_path)): os.makedirs(os.path.dirname(self.log_path)) if not os.path.exists(self.log_path): self.initialiseFile(shell) fdologout = open(self.log_path, 'a') if work_dir != self.work_dir: fdologout.write('cd '+work_dir+'\n') self._work_dir = work_dir fdologout.write(do) if shared: fdologout.write(' # shared execution') fdologout.write('\n') fdologout.close() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/substitute.py0000644000175000017500000001274100000000000017250 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # 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 jube2.util.util import jube2.util.output import jube2.conf import xml.etree.ElementTree as ET import jube2.log import shutil import codecs LOGGER = jube2.log.get_logger(__name__) class Substituteset(object): """A Substituteset contains all information""" def __init__(self, name, file_data, substitute_dict): self._name = name self._files = file_data self._substitute_dict = substitute_dict @property def name(self): """Return name of Substituteset""" return self._name def update_files(self, file_data): """Update iofiles""" outfiles = set([data[0] for data in self._files]) for data in file_data: if (data[2] == "a") or (data[0] not in outfiles): self._files.append(data) elif (data[2] == "w"): self._files = [fdat for fdat in self._files if fdat[0] != data[0]] self._files.append(data) def update_substitute(self, substitute_dict): """Update substitute_dict""" self._substitute_dict.update(substitute_dict) def substitute(self, parameter_dict=None, work_dir=None): """Do substitution. The work_dir can be set to a given context path. The parameter_dict used for inline substitution of destination-variables.""" if work_dir is None: work_dir = "" # Do pre-substitution of source and destination-variables if parameter_dict is not None: substitute_dict = dict() for sub in self._substitute_dict: new_source = jube2.util.util.substitution(sub, parameter_dict) new_dest = jube2.util.util.substitution( self._substitute_dict[sub], parameter_dict) substitute_dict[new_source] = new_dest else: substitute_dict = self._substitute_dict # Do file substitution for data in self._files: outfile_name = data[0] infile_name = data[1] out_mode = data[2] infile = jube2.util.util.substitution(infile_name, parameter_dict) outfile = jube2.util.util.substitution(outfile_name, parameter_dict) LOGGER.debug(" substitute {0} -> {1}".format(infile, outfile)) LOGGER.debug(" substitute:\n" + jube2.util.output.text_table( [("source", "dest")] + [(source, dest) for source, dest in substitute_dict.items()], use_header_line=True, indent=9, align_right=False)) if not jube2.conf.DEBUG_MODE: infile = os.path.join(work_dir, infile) outfile = os.path.join(work_dir, outfile) # Check not existing files if not (os.path.exists(infile) and os.path.isfile(infile)): raise RuntimeError(("File \"{0}\" not found while " "running substitution").format(infile)) # Read in-file file_handle = codecs.open(infile, "r", "utf-8") text = file_handle.read() file_handle.close() # Substitute for source, dest in substitute_dict.items(): text = text.replace(source, dest) # 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 source in self._substitute_dict: sub_etree = ET.SubElement(substituteset_etree, "sub") sub_etree.attrib["source"] = source sub_etree.text = self._substitute_dict[source] return substituteset_etree def __repr__(self): return "Substitute({0})".format(self.__dict__) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.9023974 JUBE-2.5.1/jube2/util/0000755000175000017500000000000000000000000015433 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/util/__init__.py0000644000175000017500000000144200000000000017545 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """jube2.util package""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/util/output.py0000644000175000017500000001715600000000000017357 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ASCII and string output generators""" from __future__ import (print_function, unicode_literals, division) import jube2.conf import textwrap import copy import sys import xml.etree.ElementTree as ET def text_boxed(text): """Create an ASCII boxed version of text.""" box = "#" * jube2.conf.DEFAULT_WIDTH for line in text.split("\n"): box += "\n" lines = ["# {0}".format(element) for element in textwrap.wrap(line.strip(), jube2.conf.DEFAULT_WIDTH - 2)] if len(lines) == 0: box += "#" else: box += "\n".join(lines) box += "\n" + "#" * jube2.conf.DEFAULT_WIDTH return box def text_line(): """Return a horizonal ASCII line""" return "#" * jube2.conf.DEFAULT_WIDTH def text_table(entries_ext, use_header_line=False, indent=1, align_right=True, auto_linebreak=True, colw=None, style="pretty", separator=None, transpose=False): """Create a ASCII based table. entries must contain a list of lists, use_header_line can be used to mark the first entry as title. Return the ASCII table """ if style != "pretty": auto_linebreak = False use_header_line = False indent = 0 # Transpose data entries if needed if transpose: entries = list(zip(*entries_ext)) use_header_line = False else: entries = copy.deepcopy(entries_ext) max_length = list() table_str = "" header_line_used = not use_header_line # calculate needed maxlength for item in entries: for i, text in enumerate(item): if i > len(max_length) - 1: max_length.append(0) if style != "csv": for line in text.splitlines(): max_length[i] = max(max_length[i], len(line)) if auto_linebreak: max_length[i] = min(max_length[i], jube2.conf.MAX_TABLE_CELL_WIDTH) if colw is not None: for i, maxl in enumerate(max_length): if i < len(colw): max_length[i] = max(maxl, colw[i]) # fill cells for item in entries: # Wrap text wraps = list() for text in item: if auto_linebreak: lines = list() for line in text.splitlines(): lines += \ textwrap.wrap(line, jube2.conf.MAX_TABLE_CELL_WIDTH) wraps.append(lines) else: if style == "pretty": wraps.append(text.splitlines()) else: wraps.append([text.replace("\n", " ")]) grow = True height = 0 while grow: grow = False line_str = " " * indent if style == "pretty": line_str += "| " for i, wrap in enumerate(wraps): grow = grow or len(wrap) > height + 1 if len(wrap) > height: text = wrap[height] else: text = "" if align_right and height == 0: align = ">" else: align = "<" line_str += \ ("{0:" + align + str(max_length[i]) + "s}").format(text) if i < len(max_length) - 1: if separator is None: line_str += " | " if style == "pretty" else "," else: line_str += separator if style == "pretty": line_str += " |" line_str += "\n" table_str += line_str height += 1 if not header_line_used: # Create title separator line table_str += " " * indent + "|-" for i, cell_length in enumerate(max_length): table_str += "-" * cell_length if i < len(max_length) - 1: table_str += "-|-" table_str += "-|\n" header_line_used = True return table_str def print_loading_bar(current_cnt, all_cnt, wait_cnt=0, error_cnt=0): """Show a simple loading animation""" width = jube2.conf.DEFAULT_WIDTH - 10 cnt = dict() if all_cnt > 0: cnt["done_cnt"] = (current_cnt * width) // all_cnt cnt["wait_cnt"] = (wait_cnt * width) // all_cnt cnt["error_cnt"] = (error_cnt * width) // all_cnt else: cnt["done_cnt"] = 0 cnt["wait_cnt"] = 0 cnt["error_cnt"] = 0 # shrink cnt if there was some rounding issue for key in ("wait_cnt", "error_cnt"): if (cnt[key] > 0) and (width < sum(cnt.values())): cnt[key] = max(0, width - sum([cnt[k] for k in cnt if k != key])) # fill up medium_cnt if there was some rounding issue if (current_cnt + wait_cnt + error_cnt == all_cnt) and \ (sum(cnt.values()) < width): for key in ("wait_cnt", "error_cnt", "done_cnt"): if cnt[key] > 0: cnt[key] += width - sum(cnt.values()) break cnt["todo_cnt"] = width - sum(cnt.values()) bar_str = "\r{0}{1}{2}{3} ({4:3d}/{5:3d})".format("#" * cnt["done_cnt"], "0" * cnt["wait_cnt"], "E" * cnt["error_cnt"], "." * cnt["todo_cnt"], current_cnt, all_cnt) sys.stdout.write(bar_str) sys.stdout.flush() def element_tree_tostring(element, encoding=None): """A more encoding friendly ElementTree.tostring method""" class Dummy(object): """Dummy class to offer write method for etree.""" def __init__(self): self._data = list() @property def data(self): """Return data""" return self._data def write(self, *args): """Simulate write""" self._data.append(*args) file_dummy = Dummy() ET.ElementTree(element).write(file_dummy, encoding) return "".join(dat.decode(encoding) for dat in file_dummy.data) def format_value(format_string, value): """Return formated value""" if (type(value) is not int) and \ (("d" in format_string) or ("b" in format_string) or ("c" in format_string) or ("o" in format_string) or ("x" in format_string) or ("X" in format_string)): value = int(float(value)) elif (type(value) is not float) and \ (("e" in format_string) or ("E" in format_string) or ("f" in format_string) or ("F" in format_string) or ("g" in format_string) or ("G" in format_string)): value = float(value) format_string = "{{0:{0}}}".format(format_string) return format_string.format(value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/util/util.py0000644000175000017500000004137600000000000016775 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Utility functions, constants and classes""" from __future__ import (print_function, unicode_literals, division) from collections import deque import re import string import operator import os.path import subprocess import jube2.log import time import jube2.conf import grp import pwd LOGGER = jube2.log.get_logger(__name__) class Queue: ''' Queue based on collections.dequeue ''' def __init__(self): ''' Initialize this queue to the empty queue. ''' self._queue = deque() def put(self, item): ''' Add this item to the left of this queue. ''' self._queue.appendleft(item) def put_first(self, item): ''' Add this item to the left of this queue. ''' self._queue.append(item) def get_nowait(self): ''' Dequeues (i.e., removes) the item from the right side of this queue *and* returns this item. Raises ---------- IndexError If this queue is empty. ''' return self._queue.pop() def empty(self): ''' Return True if the queue is empty, False otherwise ''' return False if len(self._queue) > 0 else True class WorkStat(object): """Workpackage queuing handler""" def __init__(self): self._work_list = Queue() self._cnt_work = dict() self._wait_lists = dict() def put(self, workpackage): """Add some workpackage to queue""" # Substitute max_wps if needed max_wps = int(substitution(workpackage.step.max_wps, workpackage.parameter_dict)) if (max_wps == 0) or \ (workpackage.started) or \ (workpackage.step.name not in self._cnt_work) or \ (self._cnt_work[workpackage.step.name] < max_wps): self._work_list.put(workpackage) if workpackage.step.name not in self._cnt_work: self._cnt_work[workpackage.step.name] = 1 else: self._cnt_work[workpackage.step.name] += 1 else: if workpackage.step.name not in self._wait_lists: self._wait_lists[workpackage.step.name] = Queue() self._wait_lists[workpackage.step.name].put(workpackage) def update_queues(self, last_workpackage): """Check if a workpackage can move from waiting to work queue""" if last_workpackage.done: self._cnt_work[last_workpackage.step.name] -= 1 if (last_workpackage.step.name in self._wait_lists) and \ (not self._wait_lists[last_workpackage.step.name].empty()): workpackage = \ self._wait_lists[last_workpackage.step.name].get_nowait() # Check if workpackage was started from another position if not workpackage.started: self.put(workpackage) else: self.update_queues(last_workpackage) def get(self): """Get some workpackage from work queue""" return self._work_list.get_nowait() def empty(self): """Check if work queue is empty""" return self._work_list.empty() def push_back(self, wp): """push element to the first position of the queue""" self._work_list.put_first(wp) def valid_tags(tag_string, tags): """Check if tag_string contains only valid tags""" if tags is None: tags = set() tag_tags_str = tag_string if tag_tags_str is not None: # Check for old tag format if "," in tag_tags_str: tag_tags_str = jube2.jubeio.Parser._convert_old_tag_format( tag_tags_str) tag_tags_str = tag_tags_str.replace(' ', '') tag_array = [i for i in re.split('[()|+!]', tag_tags_str) if len(i) > 0] tag_state = {} for tag in tag_array: tag_state.update({tag: str(tag in tags)}) for tag in tag_array: tag_tags_str = re.sub(r'(?:^|(?<=\W))' + tag + r'(?=\W|$)', tag_state[tag], tag_tags_str) tag_tags_str = tag_tags_str.replace('|', ' or ')\ .replace('+', ' and ').replace('!', ' not ') try: return eval(tag_tags_str) except SyntaxError: raise ValueError("Tag string '{0}' not parseable." .format(tag_string)) else: return True def get_current_id(base_dir): """Return the highest id found in directory 'base_dir'.""" try: filelist = sorted(os.listdir(base_dir)) except OSError as error: LOGGER.warning(error) filelist = list() maxi = -1 for item in filelist: try: maxi = max(int(re.findall("^([0-9]+)$", item)[0]), maxi) except IndexError: pass return maxi def id_dir(base_dir, id_number): """Return path for 'id_number' in 'base_dir'.""" return os.path.join( base_dir, "{id_number:0{zfill}d}".format(zfill=jube2.conf.ZERO_FILL_DEFAULT, id_number=id_number)) def expand_dollar_count(text): # Replace a even number of $ by $$$$, because they will be # substituted to $$. Even number will stay the same, odd number # will shrink in every turn # $$ -> $$$$ -> $$ # $$$ -> $$$ -> $ # $$$$ -> $$$$$$$$ -> $$$$ # $$$$$ -> $$$$$$$ -> $$$ return re.sub(r"(^(?=\$)|[^$])((?:\$\$)+?)((?:\${3})?(?:[^$]|$))", r"\1\2\2\3", text) def substitution(text, substitution_dict): """Substitute templates given by parameter_dict inside of text""" changed = True count = 0 # All values must be string values (handle Python 2 separatly) try: str_substitution_dict = \ dict([(k, str(v).decode("utf-8", errors="ignore")) for k, v in substitution_dict.items()]) except TypeError: str_substitution_dict = \ dict([(k, str(v).decode("utf-8", "ignore")) for k, v in substitution_dict.items()]) except AttributeError: str_substitution_dict = dict([(k, str(v)) for k, v in substitution_dict.items()]) # Preserve non evaluated parameter before starting substitution local_substitution_dict = dict([(k, re.sub(r"\$", "$$", v) if "$" in v else v) for k, v in str_substitution_dict.items()]) # Run multiple times to allow recursive parameter substitution while changed and count < jube2.conf.MAX_RECURSIVE_SUB: count += 1 orig_text = text # Save double $$ text = expand_dollar_count(text) \ if "$" in text else text tmp = string.Template(text) new_text = tmp.safe_substitute(local_substitution_dict) changed = new_text != orig_text text = new_text # Final substitution to remove $$ tmp = string.Template(text) return re.sub("\$(?=([\s]|$))","$$",tmp.safe_substitute(str_substitution_dict)) def convert_type(value_type, value, stop=True): """Convert value to given type""" result_value = None value_type_incorrect=False try: if value_type == "int": if value == "nan": result_value = float("nan") else: result_value = int(float(value)) if re.match(r"^[-+]?\d+$", value) is None: value_type_incorrect=True elif value_type == "float": result_value = float(value) if re.match(r"([+-]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.))",value) is None: value_type_incorrect=True else: result_value = value except ValueError: if stop: raise ValueError(("\"{0}\" cannot be represented as a \"{1}\"") .format(value, value_type)) else: result_value = value if value_type_incorrect: raise TypeError(("\"{0}\" is not of type \"{1}\"") .format(value, value_type)) return result_value def script_evaluation(cmd, script_type): """cmd will be evaluated with given script language""" if script_type == "python": return str(eval(cmd)) elif script_type in ["perl", "shell"]: if script_type == "perl": cmd = "perl -e \"print " + cmd + "\"" # Select unix shell shell = jube2.conf.STANDARD_SHELL if "JUBE_EXEC_SHELL" in os.environ: alt_shell = os.environ["JUBE_EXEC_SHELL"].strip() if len(alt_shell) > 0: shell = alt_shell sub = subprocess.Popen([shell, "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) stdout, stderr = sub.communicate() stdout = stdout.decode(errors="ignore") # Check command execution error code errorcode = sub.wait() if errorcode != 0: raise RuntimeError(stderr) else: if len(stderr.strip()) > 0: try: LOGGER.debug((" The command \"{0}\" was executed with a " "successful error code,\n but the " "following error message was produced " "during its execution: {1}") .format(cmd, stderr)) except UnicodeDecodeError: pass return stdout def eval_bool(cmd): """Evaluate a bool expression""" if cmd.lower() == "true": return True elif cmd.lower() == "false": return False else: try: return bool(eval(cmd)) except SyntaxError as se: raise ValueError( ("\"{0}\" could not be evaluated and handled as boolean " "value. Check if all parameter were correctly replaced and " "the syntax of the expression is well formed ({1}).").format( cmd, str(se))) def get_tree_element(node, tag_path=None, attribute_dict=None): """Can be used instead of node.find(.//tag_path[@attrib=value])""" result = get_tree_elements(node, tag_path, attribute_dict) if len(result) > 0: return result[0] else: return None def get_tree_elements(node, tag_path=None, attribute_dict=None): """Can be used instead of node.findall(.//tag_path[@attrib=value])""" if attribute_dict is None: attribute_dict = dict() result = list() if tag_path is not None: node_list = node.findall(tag_path) else: node_list = [node] for found_node in node_list: for attribute, value in attribute_dict.items(): if found_node.get(attribute) != value: break else: result.append(found_node) for subtree in node: result += get_tree_elements(subtree, tag_path, attribute_dict) return result def now_str(): """Return current time string""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def update_timestamps(path, *args): """Set all timestamps for given arg_names to now""" timestamps = dict() timestamps.update(read_timestamps(path)) file_ptr = open(path, "w") for arg in args: timestamps[arg] = now_str() for timestamp in timestamps: file_ptr.write("{0}: {1}\n".format(timestamp, timestamps[timestamp])) file_ptr.close() def read_timestamps(path): """Return timestamps dictionary""" timestamps = dict() if os.path.isfile(path): file_ptr = open(path, "r") for line in file_ptr: matcher = re.match("(.*?): (.*)", line.strip()) if matcher: timestamps[matcher.group(1)] = matcher.group(2) file_ptr.close() return timestamps def resolve_depend(depend_dict): """Generate a serialization of dependent steps. Return a list with a possible order of execution. """ def find_next(dependencies, finished): """Returns the next possible items to be processed and remainder. dependencies Dictionary containing the dependencies finished Set which is already processed """ possible = set() remain = dict() for key, val in dependencies.items(): if val.issubset(finished): possible.add(key) else: remain[key] = val possible.difference_update(finished) # no advance if dependencies and not possible: unresolved_steps = set(dependencies) - finished unresolved_dependencies = set() for step in unresolved_steps: unresolved_dependencies.update(depend_dict[step] - finished) infostr = ("unresolved steps: {0}". format(",".join(unresolved_steps)) + "\n" + "unresolved dependencies: {0}". format(",".join(unresolved_dependencies))) LOGGER.warning(infostr) return (possible, remain) finished = set() work_list = list() work, remain = find_next(depend_dict, finished) while work: work_list += list(work) finished.update(work) work, remain = find_next(remain, finished) return work_list def check_and_get_group_id(): """Read environment var JUBE_GROUP_NAME and return group id""" group_name = "" if "JUBE_GROUP_NAME" in os.environ: group_name = os.environ["JUBE_GROUP_NAME"].strip() if group_name != "": try: group_id = grp.getgrnam(group_name).gr_gid except KeyError: raise ValueError(("Failed to get group ID, group \"{0}\" " + "does not exist").format(group_name)) user = pwd.getpwuid(os.getuid()).pw_name grp_members = grp.getgrgid(group_id).gr_mem if user in grp_members: return group_id else: raise ValueError(("User \"{0}\" is not in " + "group \"{1}\"").format(user, group_name)) else: return None def consistency_check(benchmark): """Do some consistency checks""" # check if step uses exists for step in benchmark.steps.values(): for uses in step.use: for use in uses: if (use not in benchmark.parametersets) and \ (use not in benchmark.filesets) and \ (use not in benchmark.substitutesets) and \ ("$" not in use): raise ValueError(("{0} not found in " "available sets").format(use)) # Dependency check depend_dict = \ dict([(step.name, step.depend) for step in benchmark.steps.values()]) order = resolve_depend(depend_dict) for step_name in benchmark.steps: if step_name not in order: raise ValueError("Cannot resolve dependencies.") class CompType(object): """Allow comparison of different datatypes""" def __init__(self, value): self.__value = value def __repr__(self): return str(self.__value) @property def value(self): return self.__value def _special_comp(self, other, comp_func): """Allow comparision of different datatypes""" if self.value is None or other.value is None: return False else: try: return comp_func(self.value, other.value) except TypeError: return False def __lt__(self, other): return self._special_comp(other, operator.lt) def __eq__(self, other): return self._special_comp(other, operator.eq) def safe_split(text, separator): """Like split for non-empty separator, list with text otherwise.""" if separator: return text.split(separator) else: return [text] def ensure_list(element): if type(element)!=list: return [element] else: return element ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/util/yaml_converter.py0000644000175000017500000003030700000000000021041 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # 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 yaml except ImportError: pass import jube2.log import jube2.conf import jube2.util.output import os import copy import jube2.util.util try: from StringIO import StringIO as IOStream except ImportError: from io import BytesIO as IOStream LOGGER = jube2.log.get_logger(__name__) class YAML_Converter(object): """YAML to XML converter""" allowed_tags = \ {"/": ["benchmark", "parameterset", "comment", "step", "fileset", "substituteset", "analyser", "result", "patternset", "selection", "include-path"], "/benchmark": ["benchmark", "parameterset", "fileset", "substituteset", "patternset", "selection", "include-path"], "benchmark": ["parameterset", "comment", "step", "fileset", "substituteset", "analyser", "result", "patternset"], "analyse": ["file"], "analyser": ["use", "analyse"], "fileset": ["link", "copy", "prepare"], "include-path": ["path"], "parameterset": ["parameter"], "patternset": ["pattern"], "result": ["use", "table", "syslog", "database"], "selection": ["not", "only", "tag"], "step": ["use", "do"], "substituteset": ["iofile", "sub"], "syslog": ["key"], "table": ["column"], "database": ["key"]} def __init__(self, path, include_path=None, tags=None): self._path = path if include_path is None: include_path = [] if tags is None: tags = set() self._include_path = list(include_path) self._include_path += [os.path.dirname(self._path)] self._tags = set(tags) try: yaml.add_constructor("!include", self.__yaml_include) except NameError: raise NameError("yaml module not available; either install it " + "(https://pyyaml.org), or switch to .xml input " + "files.") self._ignore_search_errors = True self._tags.update(self.__search_for_tags()) old_tags = set(self._tags) changed = True counter = 0 # It is possible to add new tags by including external files into a # selection block therefore the input must be scanned multiple times # to gather all available tags while changed and counter < jube2.conf.PREPROCESS_MAX_ITERATION: self._include_path = list(include_path) + \ self.__search_for_include_pathes() + \ [os.path.dirname(self._path)] self._tags.update(self.__search_for_tags()) changed = len(self._tags.difference(old_tags)) > 0 old_tags = set(self._tags) counter += 1 self._ignore_search_errors = False self._int_file = IOStream() self.__convert() def __convert(self): """ Opens given file, make a Tree of it and print it """ LOGGER.debug(" Start YAML to XML file conversion for file {0}".format( self._path)) with open(self._path, "r") as file_handle: xmltree = etree.Element('jube') YAML_Converter.create_headtags( yaml.load(file_handle.read(), Loader=yaml.Loader), xmltree) xml = jube2.util.output.element_tree_tostring( xmltree, encoding="UTF-8") self._int_file.write(xml.encode('UTF-8')) LOGGER.debug(" YAML Conversion finalized") def read(self): """Read data of converted file""" return self._int_file.getvalue() def close(self): """Close converted file""" self._int_file.close() def __find_include_file(self, filename): """Search for filename in include-pathes and return resulting path""" for path in self._include_path: file_path = os.path.join(path, filename) if os.path.exists(file_path): break else: raise ValueError(("\"{0}\" not found in possible " + "include pathes").format(filename)) return file_path def __search_for_tags(self): """Search a YAML file for stored tag information""" tags = set() with open(self._path, "r") as file_handle: data = yaml.load(file_handle.read(), Loader=yaml.Loader) if "selection" in data and "tag" in data["selection"]: if type(data["selection"]["tag"]) is not list: data["selection"]["tag"] = [data["selection"]["tag"]] for tag in data["selection"]["tag"]: if not tag.startswith("!include "): tags.update( set(tag.split(jube2.conf.DEFAULT_SEPARATOR))) return tags def __search_for_include_pathes(self): """Search a YAML file for stored include-path information""" include_pathes = [] with open(self._path, "r") as file_handle: data = yaml.load(file_handle.read(), Loader=yaml.Loader) # include-path is only allowed on the top level of the tree if "include-path" in data: if type(data["include-path"]) is not list: data["include-path"] = [data["include-path"]] for path in data["include-path"]: # path in include-path is optional # verify tags if type(path) is dict: if "tag" in path and not \ jube2.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] for val in value: if type(val) is dict: if "tag" in val and not \ jube2.util.util.valid_tags(val["tag"], self._tags): continue val = val["_"] include_pathes.append(os.path.join( os.path.dirname(self._path), val)) else: include_pathes.append(os.path.join( os.path.dirname(self._path), path)) return include_pathes # 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): """ Search for the headtags in given dictionary """ if type(data) is not dict: data = {'benchmark': data} to_delete = list() for tag in data.keys(): if type(data[tag]) is not list: data[tag] = [data[tag]] # benchmark is optional on the top level, but if it is used only # a limited number of options are allowed on top level # (listed in "/benchmark") if "benchmark" in data and tag in YAML_Converter.allowed_tags[ "/benchmark"]: for attr_and_tags in data[tag]: YAML_Converter.create_tag(tag, attr_and_tags, parent_node) elif "benchmark" not in data and \ tag in YAML_Converter.allowed_tags["/"]: if tag not in YAML_Converter.allowed_tags["benchmark"]: for attr_and_tags in data[tag]: YAML_Converter.create_tag( tag, attr_and_tags, parent_node) to_delete.append(tag) for tag in to_delete: del(data[tag]) if "benchmark" not in data: YAML_Converter.create_tag("benchmark", data, parent_node) @staticmethod def create_tag(new_node_name, data, parent_node): """ Create the Subtag name, search for known tags and set the given attributes""" LOGGER.debug(" Create XML tag <{0}>".format(new_node_name)) new_node = etree.SubElement(parent_node, new_node_name) # Check if tag can have subtags if new_node_name in YAML_Converter.allowed_tags and type(data) is dict: allowed_tags = YAML_Converter.allowed_tags[new_node_name] for key, value in data.items(): if (type(value) is not list): value = [value] for val in value: if key in allowed_tags: # Create new subtag YAML_Converter.create_tag(key, val, new_node) else: # Create attribute new_node.set(key, str(val) if val is not None else "") else: tag_value = "" if type(data) is not dict: # standard tag value tag_value = data if data is not None else "" else: for key, value in data.items(): if key == "_": # _ represents the standard tag value tag_value = value if value is not None else "" else: # Create attribute new_node.set(key, str(value) if value is not None else "") if type(tag_value) is list: new_node.text = str(tag_value.pop(0)) while len(tag_value) > 0: new_node = copy.deepcopy(new_node) parent_node.append(new_node) new_node.text = str(tag_value.pop(0)) else: new_node.text = str(tag_value) @staticmethod def is_parseable_yaml_file(filename): try: with open(filename, "r") as file_handle: if type(yaml.load(file_handle.read())) is str: return False else: return True except Exception as parseerror: return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/jube2/workpackage.py0000644000175000017500000011104600000000000017331 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre # http://www.fz-juelich.de/jsc/jube # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """The Workpackage class handles a step and its parameter space""" from __future__ import (print_function, unicode_literals, division) import multiprocessing as mp import xml.etree.ElementTree as ET import jube2.util.util import jube2.util.output import jube2.conf import jube2.log import jube2.parameter import jube2.step import os import re import stat import shutil LOGGER = jube2.log.get_logger(__name__) class Workpackage(object): """A Workpackage contains all information to run a specific step with its given parameterset. """ # class based counter for unique id creation id_counter = 0 def __init__(self, benchmark, step, local_parameter_names, parameterset, workpackage_id=None, iteration=0, cycle=0): # set id if workpackage_id is None: self._id = Workpackage.id_counter Workpackage.id_counter = Workpackage.id_counter + 1 else: self._id = workpackage_id self._benchmark = benchmark self._step = step self._local_parameter_names = local_parameter_names self._parameterset = parameterset self._iteration = iteration self._parents = list() self._children = list() self._iteration_siblings = set() self._queued = False self._env = dict(os.environ) self._cycle = cycle self._workpackage_dir_caching_enabled = False self._workpackage_dir_cache = None def etree_repr(self): """Return etree object representation""" workpackage_etree = ET.Element("workpackage") workpackage_etree.attrib["id"] = str(self._id) step_etree = ET.SubElement(workpackage_etree, "step") step_etree.attrib["iteration"] = str(self._iteration) step_etree.attrib["cycle"] = str(self._cycle) step_etree.text = self._step.name if len(self._local_parameter_names) > 0: workpackage_etree.append( self.local_parameterset.etree_repr(use_current_selection=True)) if len(self._parents) > 0: parents_etree = ET.SubElement(workpackage_etree, "parents") parents_etree.text = ",".join( [str(parent.id) for parent in self._parents]) if len(self._iteration_siblings) > 0: sibling_etree = ET.SubElement(workpackage_etree, "iteration_siblings") sibling_etree.text = ",".join( [str(sibling.id) for sibling in self._iteration_siblings]) environment_etree = ET.SubElement(workpackage_etree, "environment") for env_name, value in self._env.items(): if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in os.environ or os.environ[env_name] != value): env_etree = ET.SubElement(environment_etree, "env") env_etree.attrib["name"] = env_name # use string repr to avoid special characters env_etree.text = repr(value) for env_name in os.environ: if (env_name not in ["PWD", "OLDPWD", "_"]) and \ (env_name not in self._env): env_etree = ET.SubElement(environment_etree, "nonenv") env_etree.attrib["name"] = env_name return workpackage_etree def __repr__(self): return (("Workpackage(Id:{0:2d}; Step:{1}; ParentIDs:{2}; " + "ChildIDs:{3} {4})"). format(self._id, self._step.name, [parent.id for parent in self._parents], [child.id for child in self._children], self.local_parameterset)) def __eq__(self, other): if isinstance(other, Workpackage): return self.id == other.id else: return False def __hash__(self): return object.__hash__(self) @property def parameter_dict(self): """get all available parameter inside a dict""" # Collect parameter for substitution parameter = dict([[par.name, par.value] for par in self._parameterset.constant_parameter_dict.values()]) return parameter @property def env(self): """Return workpackage environment""" return self._env @env.setter def env(self, set_env): """Replace own environment by set_env""" self._env = set_env @property def cycle(self): """Return current loop cycle""" return self._cycle @cycle.setter def cycle(self, set_cycle): """Update loop cycle counter""" self._cycle = set_cycle def allow_workpackage_dir_caching(self): """Enable workpackage dir cache""" self._workpackage_dir_caching_enabled = True self._workpackage_dir_cache = None @property def active(self): """Check active state""" active = self._step.active # Collect parameter for substitution parameter = self.parameter_dict # Parameter substitution active = jube2.util.util.substitution(active, parameter) # Evaluate active state return jube2.util.util.eval_bool(active) @property def done(self): """Workpackage done?""" done_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) exist = os.path.exists(done_file) if jube2.conf.DEBUG_MODE: exist = exist or os.path.exists(done_file + "_DEBUG") return exist @done.setter def done(self, set_done): """Set/reset Workpackage done""" done_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_DONE_FILENAME) if jube2.conf.DEBUG_MODE: done_file = done_file + "_DEBUG" if set_done: fout = open(done_file, "w") fout.write(jube2.util.util.now_str()) fout.close() self._remove_operation_info_files() else: if os.path.exists(done_file): os.remove(done_file) @property def error(self): """Workpackage error?""" error_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_ERROR_FILENAME) return os.path.exists(error_file) def set_error(self, set_error, msg=""): """Set/reset Workpackage error""" error_file = os.path.join(self.workpackage_dir, jube2.conf.WORKPACKAGE_ERROR_FILENAME) if set_error: fout = open(error_file, "w") fout.write(msg) fout.close() else: if os.path.exists(error_file): os.remove(error_file) @property def queued(self): """Workpackage queued?""" return self._queued @queued.setter def queued(self, set_queued): """Set queued state""" self._queued = set_queued @property def started(self): """Workpackage started?""" return os.path.exists(self.workpackage_dir) def operation_done_but_pending(self, operation_number): """Check if an operation was executed, but the result is still pending (because it is a async do)""" result = self.operation_done(operation_number) operation = self._step.operations[operation_number] if result and (operation.async_filename is not None): parameter_dict = self.parameter_dict if operation.active(parameter_dict): work_dir = self.work_dir alt_work_dir = self.alt_work_dir(parameter_dict) if alt_work_dir is not None: work_dir = alt_work_dir async_filename = jube2.util.util.substitution( operation.async_filename, parameter_dict) async_filename = \ os.path.expandvars(os.path.expanduser(async_filename)) result = not os.path.exists(os.path.join(work_dir, async_filename)) else: result = False else: result = False return result def operation_done(self, operation_number, set_done=None): """Mark/checks operation status""" done_file = os.path.join(self.workpackage_dir, "wp_{0}_{1:02d}".format( jube2.conf.WORKPACKAGE_DONE_FILENAME, operation_number)) if set_done is None: exist = os.path.exists(done_file) if jube2.conf.DEBUG_MODE: exist = exist or os.path.exists(done_file + "_DEBUG") return exist else: if jube2.conf.DEBUG_MODE: done_file = done_file + "_DEBUG" elif ((set_done and not os.path.exists(done_file)) or (not set_done and os.path.exists(done_file))): jube2.util.util.update_timestamps( os.path.join(self._benchmark.bench_dir, jube2.conf.TIMESTAMPS_INFO), "change") if set_done: fout = open(done_file, "w") fout.close() else: if os.path.exists(done_file): os.remove(done_file) return set_done def _remove_operation_info_files(self): """Remove all operation info files""" for operation_number in range(len(self._step.operations)): self.operation_done(operation_number, False) def remove(self, remove_config_from_benchmark=False): """Remove all data of this workpackage""" for children in self.children: children.remove(remove_config_from_benchmark=True) shutil.rmtree(self.workpackage_dir, ignore_errors=True) # Remove shared folder if all workpackages of the current step were # removed if self._step.shared_link_name is not None: all_deleted = True for workpackage in self._benchmark.workpackages[self._step.name]: if workpackage.started: all_deleted = False if all_deleted: shared_folder = self._step.shared_folder_path( self._benchmark.bench_dir, self.parameter_dict) shutil.rmtree(shared_folder, ignore_errors=True) if remove_config_from_benchmark: self.benchmark.remove_workpackage(self) def add_parent(self, workpackage): """Add a parent Workpackage""" self._parents.append(workpackage) @property def parameterset(self): """Return parameterset""" return self._parameterset @parameterset.setter def parameterset(self, set_parameterset): """Set/overwrite parameterset""" self._parameterset.add_parameterset(set_parameterset) def add_children(self, workpackage): """Add a children workpackage""" self._children.append(workpackage) @property def local_parameterset(self): """Return local parameterset""" parameterset = jube2.parameter.Parameterset() for name in self._local_parameter_names: parameterset.add_parameter(self._parameterset[name]) return parameterset @property def parent_history(self): """Create a list of all parents in the history of this workpackage""" history = list() for parent in self._parents: history += parent.parent_history history += self._parents return history @property def benchmark(self): """Return benchmark of this workpackage""" return self._benchmark @property def children_future(self): """Create a list of all children in the future of this workpackage""" future = list() future += self._children for child in self._children: future += child.children_future return future @property def id(self): """Return workpackage id""" return self._id @property def parents(self): """Return list of parent workpackages""" return self._parents @property def iteration_siblings(self): """Return set of iteration siblings""" return self._iteration_siblings @property def iteration(self): """Return workpackage iteration number""" return self._iteration @property def children(self): """Return list of child workpackages""" return self._children @property def step(self): """Return Step data""" return self._step def get_jube_cycle_parameterset(self): """Return parameterset which contains cycle related information""" parameterset = jube2.parameter.Parameterset() # worpackage cycle parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_cycle", str(self._cycle), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_relpath(self, value): """Create relative path representation""" return os.path.relpath(value, self._benchmark.file_path_ref) def create_abspath(self, value): """Create absolute path representation""" return os.path.abspath(value) def get_jube_parameterset(self): """Return parameterset which contains workpackage related information""" parameterset = jube2.parameter.Parameterset() # workpackage id parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_id", str(self._id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # workpackage id with padding parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_padid", jube2.util.util.id_dir("", self._id), parameter_type="string", update_mode=jube2.parameter.JUBE_MODE)) # workpackage iteration parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_iteration", str(self._iteration), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) parameterset.add_parameterset(self.get_jube_cycle_parameterset()) # pathes if self._step.alt_work_dir is None: path = self.work_dir else: path = self._step.alt_work_dir # workpackage relative folder path parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_relpath", path, update_mode=jube2.parameter.JUBE_MODE, eval_helper=self.create_relpath)) # workpackage absolute folder path parameterset.add_parameter( jube2.parameter.Parameter. create_parameter("jube_wp_abspath", path, update_mode=jube2.parameter.JUBE_MODE, eval_helper=self.create_abspath)) # parent workpackage id for parent in self._parents: parameterset.add_parameter( jube2.parameter.Parameter. create_parameter(("jube_wp_parent_{0}_id") .format(parent.step.name), str(parent.id), parameter_type="int", update_mode=jube2.parameter.JUBE_MODE)) # environment export string env_str = "" parameter_names = [parameter.name for parameter in self._parameterset.export_parameter_dict.values()] parameter_names.sort(key=str.lower) for name in parameter_names: env_str += "export {0}=${1}\n".format(name, name) env_par = jube2.parameter.Parameter.create_parameter( "jube_wp_envstr", env_str, no_templates=True, update_mode=jube2.parameter.JUBE_MODE, eval_helper=jube2.parameter.StaticParameter.fix_export_string) parameterset.add_parameter(env_par) # environment export list parameterset.add_parameter( jube2.parameter.Parameter.create_parameter( "jube_wp_envlist", ",".join([name for name in parameter_names]), no_templates=True, update_mode=jube2.parameter.JUBE_MODE)) return parameterset def create_workpackage_dir(self): """Create work directory""" if not os.path.exists(self.workpackage_dir): if "$" in self.workpackage_dir: raise RuntimeError(("'{0}' could not be evaluated and used " + "as a workpackage directory name. " + "Please check the suffix setting.") .format(self.workpackage_dir)) os.mkdir(self.workpackage_dir) os.mkdir(self.work_dir) # Create symbolic link to parent workpackage folder for parent in self._parents: link_path = os.path.join(self.work_dir, parent.step.name) parent_path = os.path.relpath(parent.work_dir, self.work_dir) if not os.path.exists(link_path): os.symlink(parent_path, link_path) def create_shared_folder_link(self, parameter_dict=None): """Create shared folder connection""" # Create symbolic link to shared folder if self._step.shared_link_name is not None: shared_folder = self._step.shared_folder_path( self._benchmark.bench_dir, parameter_dict) # Create shared folder (if it not already exists) if not os.path.exists(shared_folder): try: os.mkdir(shared_folder) except FileExistsError: pass # Create shared folder link if parameter_dict is not None: shared_name = \ jube2.util.util.substitution(self._step.shared_link_name, parameter_dict) else: shared_name = self._step.shared_link_name link_path = os.path.join(self.work_dir, shared_name) target_path = \ os.path.relpath(shared_folder, self.work_dir) if not os.path.exists(link_path): os.symlink(target_path, link_path) @property def workpackage_dir(self): """Return workpackage directory""" if not self._workpackage_dir_caching_enabled or \ self._workpackage_dir_cache is None: suffix = self.step.suffix if suffix != "": # Collect parameter for substitution parameter = \ dict([[par.name, par.value] for par in self._parameterset.constant_parameter_dict.values()]) # Parameter substitution suffix = jube2.util.util.substitution(suffix, parameter) suffix = "_" + os.path.expandvars(os.path.expanduser(suffix)) path = "{path}_{step_name}{suffix}".format( path=jube2.util.util.id_dir( self._benchmark.bench_dir, self._id), step_name=self._step.name, suffix=suffix) if self._workpackage_dir_caching_enabled: if self._workpackage_dir_cache is None: self._workpackage_dir_cache = path return self._workpackage_dir_cache else: return path @property def work_dir(self): """Return working directory (user space)""" return os.path.join(self.workpackage_dir, "work") def alt_work_dir(self, parameter_dict=None): """Return location of alternative working_dir""" if self._step.alt_work_dir is not None: if parameter_dict is None: parameter_dict = self.parameter_dict alt_work_dir = self._step.alt_work_dir alt_work_dir = jube2.util.util.substitution(alt_work_dir, parameter_dict) alt_work_dir = os.path.expandvars(os.path.expanduser(alt_work_dir)) alt_work_dir = os.path.join(self._benchmark.file_path_ref, alt_work_dir) return alt_work_dir else: return None def _run_operations(self, parameter, work_dir, pid=None): """Run all available operations""" continue_op = True continue_cycle = True doLog = jube2.step.DoLog(log_dir=os.path.dirname( self.work_dir), log_file=self.step._do_log_file, initial_env=self.env, cycle=self._cycle) for operation_number, operation in enumerate(self._step.operations): # Check if the operation is activated active = operation.active(parameter) if not active: self.operation_done(operation_number, True) # Do nothing, if the next operation is already finished. # Otherwise a removed async_file will result in a new # pending operation, if there are two async-operations in # a row elif not self.operation_done(operation_number + 1): # shared operation if operation.shared: # wait for all other workpackages and check if shared # operation already finished shared_done = False for workpackage in \ self._benchmark.workpackages[self._step.name]: # All workpackages must reach the same position in # the program if operation_number > 0: continue_op = continue_op and \ ((workpackage.operation_done( operation_number - 1) and (not workpackage.operation_done_but_pending( operation_number - 1)) ) or workpackage.done) and \ workpackage.cycle == self._cycle # Check if another workpackage already finalized # the operation, only if the operation was active # for this particular workpackage shared_done = shared_done or \ ((workpackage.operation_done( operation_number + 1) or workpackage.done ) and operation.active(workpackage.parameter_dict)) # If a workpackage is removed and restarted, a shared # operation will not be re-executed, user should be warned if shared_done and not self.operation_done( operation_number): LOGGER.warning( "\nShared operation in {0} was already executed". format(self._step.name)) # All older workpackages in tree must be done for step_name in self._step.get_depend_history( self._benchmark): for workpackage in self._benchmark.workpackages[ step_name]: continue_op = continue_op and workpackage.done if continue_op and not shared_done: # remove workpackage specific parameter shared_parameter = dict(parameter) for jube_parameter in self.get_jube_parameterset()\ .all_parameter_names: if jube_parameter in shared_parameter: del shared_parameter[jube_parameter] # work_dir = shared_dir shared_dir = \ self._step.shared_folder_path( self._benchmark.bench_dir, shared_parameter) LOGGER.debug("====== {0} - shared ======" .format(self._step.name)) continue_op, continue_cycle = operation.execute( parameter_dict=shared_parameter, work_dir=shared_dir, environment=self._env, only_check_pending=self.operation_done( operation_number), dolog=doLog) # update all workpackages for workpackage in self._benchmark.workpackages[ self._step.name]: # if the operation wasn't active in the shared # operation it must not be triggered to # restart if operation.active( workpackage.parameter_dict): if not workpackage.started: workpackage.create_workpackage_dir() workpackage.operation_done( operation_number, True) if continue_op and not continue_cycle: workpackage.done = True # requeue other workpackages if not workpackage.queued and continue_op: self._benchmark.work_stat.put( workpackage) LOGGER.debug("======================={0}" .format(len(self._step.name) * "=")) else: continue_op, continue_cycle = operation.execute( parameter_dict=parameter, work_dir=work_dir, environment=self._env, only_check_pending=self.operation_done( operation_number), pid=pid, dolog=doLog) self.operation_done(operation_number, True) if not continue_op or not continue_cycle: break return continue_op, continue_cycle def run(self, mode='s'): """Run step and use current parameter space mode: s = seriell (default); p = parallel """ proc_id = None # create individual log files for each processor in a parallel run if mode == "p": proc_id = mp.current_process()._identity[0] log_fname = jube2.log.LOGFILE_NAME.split('/')[-1] jube2.log.change_logfile_name(os.path.join( self.benchmark.bench_dir, log_fname.replace('.', '_{}.').format(proc_id) if (('_'+str(proc_id)) not in log_fname) else log_fname)) # Workpackage already done or error? if self.done or self.error: # the return value is only relevant for the parallel case, for now # for the serial case the return value is not used at all return {"id": self._id, "step_name": self._step.name} continue_op = True continue_cycle = True while (continue_cycle and continue_op): stepstr = ("{0} ( iter:{2} | id:{1} | parents:{3} | cycle:{4} | procs:{5} )" .format(self._step.name, self._id, self._iteration, ",".join([parent.step.name + "(" + str(parent.id) + ")" for parent in self._parents]), self._cycle, self._step.procs)) stepstr = "----- {0} -----".format(stepstr) LOGGER.debug(stepstr) # --- Check if this is the first run --- started_before = self.started # --- Create directory structure --- if not started_before: self.create_workpackage_dir() # --- Load environment of parent steps --- if not started_before: for parent in self._parents: if parent.step.export: self._env.update(parent.env) # --- Update JUBE parameter for new cycle --- if self._cycle > 0: self.parameterset.update_parameterset( self.get_jube_cycle_parameterset()) # --- Update cycle parameter --- update_parameter = \ self.parameterset.get_updatable_parameter( mode=jube2.parameter.CYCLE_MODE, keep_index=True) if len(update_parameter) > 0: fixed_parameterset = self.parameterset.copy() for parameter in update_parameter: fixed_parameterset.delete_parameter(parameter) change = True while change: change = False update_parameter.parameter_substitution( [fixed_parameterset]) if update_parameter.has_templates: update_parameter = list( update_parameter.expand_templates())[0] change = True update_parameter.parameter_substitution( [fixed_parameterset], final_sub=True) self.parameterset.update_parameterset(update_parameter) debugstr = " updated parameter:\n" debugstr += jube2.util.output.text_table( [("parameter", "value")] + sorted( [(par.name, par.value) for par in update_parameter]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) # --- Collect parameter for substitution --- parameter = self.parameter_dict if not started_before: # --- Collect export parameter --- self._env.update( dict([[par.name, par.value] for par in self._parameterset.export_parameter_dict.values()])) # --- Create shared folder connection --- if self._cycle == 0: self.create_shared_folder_link(parameter) # --- Create alternativ working dir --- alt_work_dir = self.alt_work_dir(parameter) if alt_work_dir is not None: # Check if given work directory contains any remaining variable if re.search(jube2.parameter.Parameter.parameter_regex, alt_work_dir): raise IOError(("Given work directory {0} contains a " + "unknown JUBE or environment variable.") .format(alt_work_dir)) LOGGER.debug(" switch to alternativ work dir: \"{0}\"" .format(alt_work_dir)) if not jube2.conf.DEBUG_MODE and \ not os.path.exists(alt_work_dir): try: os.makedirs(alt_work_dir) except FileExistsError: pass # Get group_id if available (given by JUBE_GROUP_NAME) group_id = jube2.util.util.check_and_get_group_id() if group_id is not None: os.chown(alt_work_dir, os.getuid(), group_id) os.chmod(alt_work_dir, os.stat(alt_work_dir).st_mode | stat.S_ISGID) # Print debug info if self._cycle == 0: debugstr = " available parameter:\n" debugstr += jube2.util.output.text_table( [("parameter", "value")] + sorted( [(name, par) for name, par in parameter.items()]), use_header_line=True, indent=9, align_right=False) LOGGER.debug(debugstr) # --- Copy files to working dir or create links --- if not started_before: # Filter for filesets in uses fileset_names = \ self._step.get_used_sets(self._benchmark.filesets, parameter) for name in fileset_names: self._benchmark.filesets[name].create( work_dir=self.work_dir, parameter_dict=parameter, alt_work_dir=alt_work_dir, environment=self._env, file_path_ref=self._benchmark.file_path_ref) work_dir = self.work_dir if alt_work_dir is not None: work_dir = alt_work_dir # --- File substitution --- if not started_before: # Filter for substitutionsets in uses substituteset_names = \ self._step.get_used_sets(self._benchmark.substitutesets, parameter) for name in substituteset_names: self._benchmark.substitutesets[name].substitute( parameter_dict=parameter, work_dir=work_dir) try: # Run all operations # continue_op = false means -> async operation or wait for # others in shared operation # continue_cycle = false -> loop cycle was interrupted continue_op, continue_cycle = \ self._run_operations(parameter, work_dir, pid=proc_id) # --- Check cycle limit --- if self._cycle + 1 >= self._step.cycles: continue_cycle = False if continue_op and continue_cycle: # --- Prepare additional cycle if needed --- self._cycle += 1 self._remove_operation_info_files() elif continue_op: # --- Write information file to mark end of work --- self.done = True except RuntimeError as e: self.set_error(True, str(e)) continue_cycle = False if jube2.conf.EXIT_ON_ERROR: raise(RuntimeError(str(e))) else: LOGGER.debug( "{0}\n{1}\n{2}".format(40 * "-", str(e), 40 * "-")) # Delete parameters, which contain a method being # a function of a class. This avoids excessive memory # usage when the data is sent back to the main process. # It happens here, that these parameters are static and # therefore not changed within this workpackage execution. if mode == 'p': parameterDeletionList = list() for p in self._parameterset.all_parameters: if(p.search_method(propertyString="eval_helper", recursiveProperty="based_on")): parameterDeletionList.append(p) for p in parameterDeletionList: self._parameterset.delete_parameter(p) parameterDeletionList = None return {"id": self._id, "step_name": self._step.name, "env": self._env, "cycle": self._cycle, "parameterset": self._parameterset} @staticmethod def reduce_workpackage_id_counter(): Workpackage.id_counter = Workpackage.id_counter - 1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.7615216 JUBE-2.5.1/platform/0000755000175000017500000000000000000000000015273 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.904386 JUBE-2.5.1/platform/lsf/0000755000175000017500000000000000000000000016057 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/lsf/platform.xml0000755000175000017500000000701600000000000020434 0ustar00sierrousierrou00000000000000 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 --> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/lsf/submit.job.in0000755000175000017500000000120200000000000020461 0ustar00sierrousierrou00000000000000#!/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# ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.9073732 JUBE-2.5.1/platform/moab/0000755000175000017500000000000000000000000016211 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/moab/chainJobs.sh0000755000175000017500000000052300000000000020450 0ustar00sierrousierrou00000000000000#!/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/moab/platform.xml0000644000175000017500000000737100000000000020567 0ustar00sierrousierrou00000000000000 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/moab/submit.job.in0000644000175000017500000000123500000000000020616 0ustar00sierrousierrou00000000000000#!/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# ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.911369 JUBE-2.5.1/platform/pbs/0000755000175000017500000000000000000000000016057 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/pbs/chainJobs.sh0000755000175000017500000000052300000000000020316 0ustar00sierrousierrou00000000000000#!/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/pbs/platform.xml0000644000175000017500000000717600000000000020440 0ustar00sierrousierrou00000000000000 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/pbs/submit.job.in0000644000175000017500000000122400000000000020462 0ustar00sierrousierrou00000000000000#!/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# ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1661329994.9173625 JUBE-2.5.1/platform/slurm/0000755000175000017500000000000000000000000016435 5ustar00sierrousierrou00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/slurm/chainJobs.sh0000755000175000017500000000113200000000000020671 0ustar00sierrousierrou00000000000000#!/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/slurm/platform.xml0000644000175000017500000001023100000000000021000 0ustar00sierrousierrou00000000000000 sbatch submit.job ready error srun shared ${shared_folder}/jobid ./chainJobs.sh false 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/platform/slurm/submit.job.in0000644000175000017500000000143000000000000021037 0ustar00sierrousierrou00000000000000#!/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# ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1661329994.919361 JUBE-2.5.1/setup.cfg0000644000175000017500000000004600000000000015270 0ustar00sierrousierrou00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1661329941.0 JUBE-2.5.1/setup.py0000644000175000017500000001177000000000000015167 0ustar00sierrousierrou00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2022 # 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.5.1', 'packages': ['jube2','jube2.result_types','jube2.util'], 'package_data': {'jube2': ['help.txt']}, 'data_files': ([(os.path.join(SHARE_PATH, 'docu'), ['docs/JUBE.pdf']), (SHARE_PATH, ['AUTHORS','LICENSE','RELEASE_NOTES'])] + 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)