JUBE-2.2.2/0000775000175000017500000000000013426052212012127 5ustar sebisebi00000000000000JUBE-2.2.2/setup.py0000664000175000017500000001176013426051426013654 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 add_opt = dict() try: from setuptools import setup import sys SHARE_PATH = "" add_opt["install_requires"] = list() if sys.hexversion < 0x02070000: add_opt["install_requires"].append("argparse") except ImportError: from distutils.core import setup SHARE_PATH = "share/jube" import os 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/jube', 'download_url': 'www.fz-juelich.de/jube', 'author_email': 'jube.jsc@fz-juelich.de', 'version': '2.2.2', 'packages': ['jube2','jube2.result_types','jube2.util'], 'package_data': {'jube2': ['help.txt']}, 'data_files': ([(os.path.join(SHARE_PATH, 'docu'), ['docs/JUBE.pdf']), (SHARE_PATH, ['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 :: 2.6", "Topic :: System :: Monitoring", "Topic :: System :: Benchmark", "Topic :: Software Development :: Testing"], 'keywords': 'JUBE Benchmarking Environment'} config.update(add_opt) setup(**config) JUBE-2.2.2/RELEASE_NOTES0000664000175000017500000003536413426051426014123 0ustar sebisebi00000000000000Release notes ************* Version 2.2.2 ============= Release: 2019-02-04 * New "tag" handling: Tags can now be mixed by using boolean operations ("+" for and, "|" for or), brackates are allowed as well. Old "," separated lists of tags are automatically converted. See Tagging * Extend parameter update documentation. See Parameter update * Platfrom 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-pathes 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 Older JUBE Version ================== * please see our website www.fz-juelich.de/jsc/jube to get further information concerning *JUBE* 1. JUBE-2.2.2/docs/0000775000175000017500000000000013426052212013057 5ustar sebisebi00000000000000JUBE-2.2.2/docs/JUBE.pdf0000664000175000017500000107657413426051426014331 0ustar sebisebi00000000000000%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 195 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 196 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 198 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 200 0 obj << /Length 320 /Filter /FlateDecode >> stream xڵN@ y udzZuހp R. 8HT+-;0?8 pM̞\_uEІP:y _zFt֢3(jG`0> 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 205 0 obj << /Type /XObject /Subtype /Image /Width 200 /Height 89 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 39 /Filter /FlateDecode >> stream xmH@|ںF endstream endobj 208 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 3 0 obj << /Type /ObjStm /N 100 /First 827 /Length 1544 /Filter /FlateDecode >> stream xڭYMs6W`_$0Ln2ԍi$bd5R}KǠ xbRe,g2cǏqgNJ0qäҌ[ Ld"gZwXRff际-S0& UHph8p9mpYɁgIrY<ρgpE䎆>/;>c]M(PQ9sGȵ_fγ^ ȟO311g;Ԕ(*Vvݙ+.DpBY^Nze ҫMgxO]1432v~WKť?[?(4$<ȭhl`2]HZToknt~F\<Ѧ7!pN>saY@3ZZ>dߥYYBG%ԟZ]ND{7ٙ=B[Oz7KfP ,jŁK@kecxث_`);dU:X; 6C C`Ty -֍7G 9K67{]jO-c.BЊ[ {7fqѩc4-*Л=EU7B;T^ZB;!Ț3k};/Gug,=q^.+UWjb"˨g~V)UƯhQ(~dH gMY}5 ja-KOFzVV^-[7ꧫ!|oVVff>v(~*}̊:~8m;]pF ?;orTg͚z 0gvY`?a endstream endobj 254 0 obj << /Length 1282 /Filter /FlateDecode >> stream x[S6Sq C2It<[,|צW^lsˊ, z F?pp>̀[~y C!DayCg9'?> xpopjћ|PcSO0Ěj Ǡhz *]yTUgLռMYt,0H?wGQ0*/ Ao.T,Bl I",s `ggT`iyh]1! ć5_e?`I4|24ƐcHcfyϽdxR <(TVlmsSloRNzZ%+WVc4e%K$ E2 c>^k` uˬ\k""ml馊Uz5nɭ{:l N9 'ZȀB2j\%LUAtƤBݯ6<׺;M<ͅ{ eԥTMh2TXC˔F\e\\O9/Q1X.X"56/5^E.+UwW̖!̘1cq~T2Blqd}.,yEɄc!ʜE5ŀ%jR]}T&ov3$:|W(6}7Мt0l,*oٳL"~{XR~Uƒ\6M>r3|bCqDP< C!j7C|D endstream endobj 266 0 obj << /Length 323 /Filter /FlateDecode >> stream x;O0wƏkEn@Cд=G&P";w'Xg `r7 -Ha<%w,ŵEtARH{(+gV(`e֭MO i[)1l&+!3>^4sD$;*)-mS!:!" endstream endobj 270 0 obj << /Length 1144 /Filter /FlateDecode >> stream xڝVK6 W(ԪDRuLaMS-JR8 ^g2$AG!JwiE%"Mˣ[|F;?EiRUEtZ]$,ae6{vWQT+>ed-E_h>wks~e3pQ0$݊ʂ=j.7WeR0+]8OuOX<>>o/YnVVykJN:+VuI9 rݍqwL5a7z[N#mTC7J#/Վ2On\C'zfG> 4Fe ΧE~F\U?Lln0*htN,xW(hɰ+)C,2†B\y8w;|:A C?) 40q`lYهMM,5seRqNMq a St(K[b۪RW/2eKoo.+YQhe1,lk> stream xڕ?o0w񟕖Vd 1DH4_7. '߻3=p`3ϦҁCI߁!0΢|\҆n{I*BRCO%{vb"n [rh^G,\W-B |% o_SvA8 EUĩQ{C8':Den8ij?1RF_BUN endstream endobj 281 0 obj << /Length 2474 /Filter /FlateDecode >> stream xZmo8_,_DR\h{Mmh fb]eɕ7|ӛ7'@LԐy808p _N|DFR`S'D)%+;jŜ *菓\<SBJP-/XrD)cor}:{ny~NbbAxh*!eH7z9ųfrF,5&B8! Qf}|Ae2[~XcX/~uK漧1i^pdU\n^x웲=ޭN(e6|uNLriKQ}ʹG o_?Ol:W")A8a:UHE d-1_&HT9#Rš EpP>.8ƳEݤy6YY $tÀ9;{eUzibUh?aԻfSz[S$'%I+&D 0TS"7 ?aAj@ =lˍ?xְ0 v(3"H19' g`L,ҵVzB cБ0"1AZSTb qNs+_BNU֮u4ܤk*:%1*J$]Ҫ֋mzc?,LK;+-=Ԝl".lA;v@WiPfWݕqPu'f8z4Z{LQݧ6-NhA|Y?f Q %5?goby61ZnuXEfEm:> E]BMLѷ].]}PeX9cR!|>$ &`}*B<.̰ ]pk$;P`N& C׸螓*|@;daa̧ 1rfrgHA czZKг}]a*62ln?pId>=ʣ -J1+KL(DSi6]&+]9uLDp ۃȁ?M!{C6Z7]'oX/\x/zJzSIKpڱN6f#m",$[y(n)"c`y1bjv9a$a߃ʨdlTfx8 ?3I/* -68oP^ҩ+2o lz.@-ө_N.w= fCo }ٷmUfjbR)nRoX]'' f^jx4{g16& Ľ Bѐb7Ҏ8dL_,>1`n(│4 \oU7Cmڌ* kr{g\XUt] b2ž+&)腗SO0S2QeFqCLo]{ĭql79٧}eMvAe'l?eMq`'Nնc)uP6Ew ~w B!IlY`\"(bDw)ÇAX(!to䣣cdB!s8H|"L v*F"n1-BG?7zy\64Կh/LzG"RWH!RaupP@P(;M#dD@JX$@I$~6jTo}.=M*W%(s?]]ѧ${e.r0W'U)zr%{\d+3߯}#i_d9W- meM(#mntՠZ4~(XtOɲwIKѰPtm!yk(/uф NEF:+1f#fDVZ7VW~MW"~N1:Հ>~0z7 khzzUV7! 7FϕZSUpk:GDxJ endstream endobj 289 0 obj << /Length 3495 /Filter /FlateDecode >> stream xko6{~/X3")J!mK[u,C[-Ǻڒ+ɛwCR˛M; )`$|uKLD"\-'ĉfRbw.f"&+ͽYZg ;˫?N8L8A J|sod_O&=36Pq&ɏ'> ,AS:jB') /#Q/Ѯ/ח| HX+tgQEEYm=,]EM17yq^F !C) fvO/MLUFoo3낦U^p^n6i,bzk~-x0Mw̄g2(8H*ӂ7Xm▆ #+ ,Ph=VyzXœqucHTA~巗OrP:A}oLpٻ<2w[mnN%֫S7Y1_m=7wxXKhU~=`?BQRd)"ڛ]Јd4w r4@mA5bqzcQڦ)n_>J#E=񲻁=$}2@ NGa|8O#<5ӊ TI=ߗ25eF۪V&PK,[dYG*)$ TboW0xpE!& }?eޯtbbz4[<{~Hw7w^^(}tviSΩYiIȾ 0R^zbN/cdd- )%H)GڪՕF{.Xp6cYVs/YV`|1{"Pi`}qMy}X>40ʞL x߾s< =.؟ %)' Q?f>0Cљ^%,|"f81~&Ka׿0D|6j'D] B:H cC)g)M-`n>=}wL~BE;c g?ab70#2}!?o߆ }pB9BF6nI*>GPGO;Rzklf4 E!^:="!A8-Vb46>Nl< !^$s28^]O`,Cs{8P!xo9fpWg僱C@ѐ,}QvWSfn7 kdeͱ+&h\3٦mOQ !%e ;f?I6Dsu?}\{J? УX$+G[LDdc\Rv]O-,=S,H' |`@kj 2fQ"igЙj>=O*2jE|e-']dz\?mߖL9ƧdͧQO9DeܼwCE7>; |N%D+ř`O@D##@n@([ <q|D+?T iݤUSy&%0XsIVQKoXQ854[m?(Ue&+-u^J(Htdg3 LQŖۨqPaecxL%~2E EiN1 n륛ݺ}AbJo]Q)C9Ri9*V|EyY4i-|lfA9/ٰk @*E4 )fWw%W1D^r{ 2M O[DtTJJX :$_dE/*s]0xDحG ;d_OeoL{oo³-'4m*^Vtl`(W=-98q Ez-fد"^oL;RMF=FULSٽjz嬒tB !K%.'Le Ku90D0K5f֞|5ٶHן_ŵf{*`Үz8mWD\Jq09u1ܑe=t&Ou;:Dj { Z{MfUc!7:'o&pLp4S9}j-A\H LNN=U6nEMao)x0hUto *¨2zլ҆\"Dr40U ,$oXL𪞩zeP|u6r42nӗ';pe-U3cl7 갠@:ṈhզS8xL&WznʀC Jc).8T,jCäm텥}IBokl"V% ` >/)9R.~B#jڻ$TGPL,z]ːr Zw*]!l{ePо+o?8!t[f{|&烂G`:&RN )T8q6I8xh bal!cu&X]Pe)E'$}1X#`7G3 |Mf=r$u>ϛ}/j} Gy@ʹ CPƂ_-H64;r]T endstream endobj 295 0 obj << /Length 2355 /Filter /FlateDecode >> stream xڭZYoF~`0D= vdfĮj* n0śx$VLWKNx((քk\ϋyab^\Y$M^Xڧl%ufo򗫏.~ & h1(`0H1 upgBR"]/¾RD~nYd5u7)ZRϚ!/z>$Jv?0Ih$=[2\8}\څ]Y݂*na ;d$b iZ "1w[m:i=_'\g f\84ZpQL V/vuy r>.i,eAULB>$d!#$1΂ )'f+lv X+'|ajX %\UXK[` dN+fj)PѶTt$$abʃ(tł%ۂ5ab\gq橈H.?V-wyoƓf66QpIAB%6~c"vP1 Ǩ;6iy>pxbNYR>Tu5Jַ85PwR򍽚䍃;Sw 7*`QHP&+ %3Et5VsTMZL ֐Qt[f (C9E9Y.,v*sAn`׿|@z W++j j^zo2((_=ߖT/nˑl3NSe3z@YTf!fP}xXg0)gq*aT8# P8"rʴa=j3 P {u"NV}.w2#Tΰn܃)ҲMXWF!EW 61}Yd!{{J)E )RB,)%d`mm=]en{컔|ϭ>=Þ]}^Õ%xJ}(躭pG)>`{lLCOT+>!-H麸Wgkw} ǡW6[O MQJT7jQHEaV6< y7K|xUUYs_(!}(ψJy<0 ma/ 1y v?i`7kWoBwB3&ڋ늘0U>Ζ*fܒ۬6Մ3|WξR> stream xZoB@'>%vE{)boQ(b+KI Ce9]Q497oޏ7n^LbŔw {wkX,,W.-;iR-}pw +E8V_o 8̨'$%,TνnvX{G=*&L($~!C$xD")*`c:PN{I -'"hRH(aO;/}YU~KuR ~.+lҺN6io>:Cm\P_XlֆzīT_#cP pʆ8eSCOB\J(B%I2"0"8IBs$65|QԞ ]q} {ʴnԺ 1"-b;w3,Q(|=l &M@jlK*Mvf@Ei9 X3fQaPɢ.<)J3Y4_&%3.e_1g 87.U8^m5GʶLW}?od\鮺e1Vc:`Ekv'ҺFv41o&=a}Zjxfq`KAD8qݱ "]{$3wHn.+MKޗV|STz>D#:cٶ+`C_X vw_Byj^tKbŵ.XHDq`Bx})c=hěE2GJFreta W\ST:K2~ gIϧw=6y>DQ ņxs8[-8~ SGqfq' Ћe!xOಳWU4,K*UZcXGĊg2W3 PI:k5  M 7r,4TFr%mUṬ}A̔j=чakY }зIlNJF1vn]t2Kifx/Itb 7n=cH6[?r%Hޗڊja6xQdH[}VXi.j|xck`7 9fDA[DXI0r18uGXȖkR*C{$ p`^<Ο;{ʵq؛r3 ~zeolp iРP^BEw ZڕgEc|aS%M/Gwcnmm9'Qlm&ۋUT?|Gs]|9dځOH*h>DP;\h2ges@17Շ pƁr&MEПӶޤEZb8u&mA,ΟeH4 Trz` D) \qH6̓rr<7FV ԫ>xW0B)\z aK/v!06ȷReI>eb endstream endobj 310 0 obj << /Length 2583 /Filter /FlateDecode >> stream xks{jxټ&iޗgᒹ$b# ISԝHv4s$Wboipo_]ܾqXq-!T:Dp7\ovm*O/{9.}N&.څ:`Ġ*no`ᄀ| Sr?С:F*Kt%cз֝ Q‹}M<\6I<˔6\rFbyLyHnzp~Ne-`@v)BgFH<G {."ۮ8:DT#N5qS*=d~g"QnQ6ܵ2画u4*/[D$&mwZҗ2vn/sc]/+9ժ'U㭪L.Q~ժϭ}nq6ͱf펁'ۀDuSL,NULE6"BeAdhv ๎^K,%^[w!86?@gyV%iVֿ-2!&z3R@gMG*2#~[TumQ$einK^^-Ǫ `MZ:BxږR*5w^ ի8v05`Uhœ * 72I&Iv6NT=4"ՕnN_=,rlp&/ҽYٺ88 MiIx;[,%]4 ߭/m@J]LƿZV>sTZ-cYԞ3n mLu;Kxf65->Tխ:/BnGMXi[Y9pȕsIaAcU9җƳvHQucY 5: |c!I6:yߋ@ߟ;.jq  YDl#J= <6hu$?x9Y J3;%d!\G Y xlHn/%n{AyV7iU簡]Xj&?_3f6/W.}qAдC7֞_DfCFݬUzы9 ؀=`p*Mz6E>3e\_y܏yELBd#~J,,RX3Jܺ fIx!%㰉%v2$R@~-?Mt.{!WA4 LA'4 M'fHf6MڵX(z-#S/ uu\#P]s8.{m!3+pSVIQۇ"lh;b;48xku9O6ׂ2AtAz[UdtlazK,Tf^B>C17ΉTTQH_w0ߤh{fU9> stream x[6^ܚˇǡiA%Oi6w-M;á" (r8 ͽ{{?^yb/fq  Snw/^^MeMϷkUI5ս1+^$W޾xy{'tz_g*jZ0P^yo/{~/=*ޚv f\C 9z_Lj ?bxw0. hkOfz3QbZ^+E! oS+ǐO'٘Š(nķId4uyqS& ݣnTݰ8fm|L*Th&Uv`#7E~E\D| m3S@,"=f˥Yh|NW]j3!geacI*gz,d ?Td%T™mjlC _I3_1NJ $o;3aыiEBvCq ob kZ6$90允f!ן"-!'9ZH Ad$',*'2->nWBO>&ЧCXa]BU>Sm`-@cKiq (\f n}`tjh"gl@Xi(>aƀzZe,0Wp”}=3 #g ˺CcURT0Au8{*s*-+'9UuǷfqS5/ YP٧} ,n%xOp &ij";iT0&F0rľb]%npчW$ MN_.uzltfC%nU zH!v{I[DYn;×vݩҪS(‘Km3<Y.LӞ W0pMhFyn4H۾;bٕim=qS{dgJM;wRS*8"35xR2!{֤4j ;Ych8;U/ǘB l:ATiqhEM]y"QkJW&sP` ls̉@^[5 *̠t/< x԰bR@$>V8`a+D}|X%}!ߥtq=L,ϷԢfu35vv;#dHSI'}iRR[!_b:5~]I=3v9`:ѪG 냀DA֏9@6P=P*`.Sp=yWUE^jyFq X"i?;t:t֭YDTͫDߜzU|"wjyG;ٚu%rsȚL$`5"}5kcmD ;a\&ٽУck,`Ȇک sY3];N,o; 5Hrm0Տ8>W ||D( k0E R0~霢l#?|wj]~&L?8S7M]5y^ds'yAIi,X|+~i[1ZN*  vܶԍ5Xy2%)SOokꞚDn:>DCe%D1 !~}&GdU j%`"seu (EzK '^ endstream endobj 321 0 obj << /Length 2644 /Filter /FlateDecode >> stream xZms_$ O#:7k.ܴis>t.E6kTI|ɏ.m0^;y{y l|zŀHyïwOxz歕JpyOu;1}yyٛ.\ˈ.$P\"Lp>J]mB I <Õ Qz!hTQ<%j<-LEu@:'%?X@h<݆hlKlKߟư{q5u,hqzJ'Nb 98ÍO"9}1?S͢zSsRVq7>Vktfq, $a5vMðO?ۋFRWOfTD9G:Z4U>>!Uk]Gu3ĝ)xu pW( *dS'!Т> VT 브WW[GB6띟GWDVc<hn|4OԺe{sJC->$2W^ğ,"3x8!ЂM_ +,^씈NI\wPKhExc"Ưnu jjp+aqXQ@PQ[<]ݡ$f1r詾{f 2$!e~\o{|;Wh[h(" SRol ѳHIIbQ q.oqD#y>3;0##@ˢIJ€7r=dn34 *<$ Ni FZsL ʺyӋC@hL &88(/C)戻ǼJT&g h~nsU H In}&gbmWG/u+T@-S:sT,כ!x|g/"3|Z@-YV {9#*1aԲB2"Ը%Нp.)SP(qskHaUus 澺-6/pJ[3o .ONI]V瓼j2{fL5%ĺxB3,$,vhV3AZ18gюqŋNJ׺y\բ>!ᖺ 7Tt>Vu{z{bt%%)>2ŏe4 TdAkr>ͼ/Z/f7 C G!ADESۀ(iAPi.OihuT$jbSZZAQ5!(Z?ugn`й݉~)88)of)R4^ q9y, }q=>K]%ejBj]7e} >ZE ,CK,Cc5Nа*S@&ÚAc.Z\̭8 0C$r\o0lൗuU llF ,½.&(c^. a.)d͜u;z)xÄH`}BmѻܩMKw23hutR[MTA!d"G}K8~#MGm}s6\cILB+y V J4Jbo!0 ⍉LY%BFtKcZxMI崁$l*"*%aӶePZ0+¬Piԗ`-hLe7Uvəe7+LB׃poPʾArmZ 2pZfމur'-զi[$vV\ZsS'j'S„ 2ݶ?p٠H4Sm 75Z4д˴==C{ 9rX2AWwn9ӶףCY]E j!vsQ. {ŸtxU$zL$/UxJdd[n_ *Q9Gs" qr@EZN;6I.u-ESNcVN]ѿO`i6.62n -|p6y]j/5ӂG&㑖pefZSBenW%-qe[&pMJS6ɵ7mjs7vo15ssWQ=Tڙ|ZY[g>VEyyrcԝ(8U*~BCc~bjE.gҖR9u`NC]H*mCȖԳ!Pհq uQ }J^>z7~w>]kعQiΧY?> stream xڽZKoCOףd J $!)@AF^HB .ARհpΒۛ93=5ͩ8K` L)h)R( ġKhp@)e@>#5ZNƁ9jxLLA1~XED"+f>bA%)9cBAATszJTW9HXYJk%҂.T sm}A%VP>!'_]sȜ>;EkZuB.#rD) !C n\`$_U"DWVB: )0\r.5TO(no,TWF+XCU3. 7+Wf;v z~MTLn(Xj0` WLb|gEan UW0ʭnLadjxeɗ)̂r;PM!S}Zvj2 ߸`d7$)T07$Ba| )U_:݋m2=f^ʽ1!l5zyj}ބ/Տ??_1t~~\ܬ/na o}^7ޭgO?›l9k cb+ngP|3;=FfrJj~-Z^N;=N;=NO:=ӓNO:=ӓNO:=NO;=NO;=N/һYէxլv\`b`YQ2)hb-* = ggaz?o^o<|*, 墔X̑FZbNy/<93rn."6JE2@~-/qq"J12 !1%ix6Rsx'p &KGRVʜ4de< x7鱦%q|IRQsBV -zI)y%"p f(/1HH) aK 1|`RV#R4RZxwR WN٢Gx9"L6 Dwm(-r[xFȄp(lKGSAGE)z%z0^c<2<{1@~Q(p0.QԲ(pU=e7d@H as8ュFFj780㫷/`Ls,? rF1?PY`oUěF+>yF+F! Y`C[(![کDsA̷{mt|A K#F=J !6>,(G=-ڒ8N`Mo/8;;D,c<G sb.N` azTI( 1-1>=67V- Yl#mnG%aO rs[O %Yl+TDioǡzö\K۾NK7sn;z&@);EȽ{S&y7ɋlvw=֌p(J"S,-EEU ^.sEH,KEِ pcƼ(2H6&c0%Z#ˋp^!rߛ\ ӷoQۋ8?H8%/#`܃; T(iJSa5ۀzഇpwKr- kw)([Fay|;“ : "_f~7}"Rs~K9\$`?5bwٷ0A@V$fЏ;!zV݃Dw4T=Rc;VI*ZR,ߊϟi* BEub4@"|E*g7 D[s?3?F ہ~PJ~o9q˖<N/G  ͏-B1swaIڧ|Art |&v5bFh$ۘ=" #A8[*6+ +P endstream endobj 335 0 obj << /Length 2438 /Filter /FlateDecode >> stream x[[s~ׯ@U?PSsw,2r2cC:U'7T@ȍVR[iFX-ݳ򝳋8X8ˣs$M bߟif?FgU\%y]TǥvP'\~8zy#39Q,6G~?HwS h(;b{Xq#k~5{vaBdČQ!EXP$Y@O=)(.).4?Kg2Z4OBj]./>Ƌw *nu:޸ (6  FܤYK>3; G۪}ȸVnA%)cC)P}L!~pGC_0ˆsby#%'CX((R0' >X]!h ] ]nʵY; Bp1 RZΓ,N)f 7q\Y2?!wo:[7q٢G4Ϸ]ҕ@ٯ'DtˤЋ*/]Z UKB;rj\.Plf+^&6rqnS4ե3 @y;.*gJ/)gY_5ىZ6|xQ) ę]UZR4Z9+ÊdgJXH c; 0?TB15.׺)-äxsGq׺~h[ deθ-b vN+ X[ΐ DM(y`uIv H@=9ЗM:$ aH#rL&"K K  8sB̡Q;2(#$DVyQMI9&.| \mf.\x=Ϥj_LZ?zLx0/:l|րco8*jEJN[W28}'7ͣ{z>#B9EOS+"Ϫw]Iv<]imzRۥ ȉl+ Z rؗ o@X}GƢ#Phrbȼ+ {Ym%:fKA0J4ͻ9dՁ2 o# '5*.X(*k(BF)k1èhp;B˭~qdY۩1(5>WI^A*N&Bm FM9=6ºwv "O";F~3ĸ[F'' y O]/ z:w8Bn'ƠBLvONOҼ fc΄!B7|I,NP9H$qPyy^BqsH|~{h k'[x9vH"9M>NljpoGZDxj'!]y:Yroӥk_"2w(O}܌_xKϔۯ^lOR=YIZIxD&ST>+tQm~yQ˳)R֗i_ endstream endobj 341 0 obj << /Length 2950 /Filter /FlateDecode >> stream xZs6_܃T0>ܹI/}hR)d(PJRibg?b_xt至o.^4JYjҊ)$LY]m7R&v}>˺ygmNՏg/~? DH3&zw60cęJmtfX &2g|̦#6Lú*@7ٹ?΅^ KYt792tҨ0@ޭϺ.oK"RvyriaRvcl74҅W~mGV*~?[ R6Ms.EI)X ͻ˙&%J+;-aRӻ>)5KE Z{T_u4L% FnoZ$hxl,d]eI-']x c$3-mC5c&o[ BMeΐD.5C:p ` cm@AE $$al/s o 04V9SWY,U}{So|9";Z+am3--Z;889omv(;(M٥2M%˺MQ]X,tgN1ںԋ$,b5_0Sdqy>'b9>o-}Vx,x9!}t'K{h) Mx54۲ΞBm KǪڐ pLc_nhA7__{|6Vorc\Ep/(fefک}E_Lx_{p|gmgMƉi / b}@j !m6 X8vq#$\(842aFz dژxc;N q4^AZSl|j!~%aݱ>Р`3D\j X\%|sX{&?},ֱ/~]V:ȜIBOTh*69H8=W> @3JO\[dצsbl:ڕ=abiYS o}.GAR'&XԇrC+?R!XZ_EM]ĔP;e~" c7s)qQ@։xZ}8$ZM^#pqS Rmbs eFͲ٩-Qm|z v1k=)h{fbYR+L7Athpm$n>Pee!ݪ2pUO+:M Fۨ:ż]^CWkZP(cFzᕗb*+B.|qm}^m:p%\yKpJ'M{~~'*ݚwjzFaH]^1RgRI Nr 00 hMO<W@pC~j4!yK}1}Wbj}}h3:?})ch|Ec1AIy Jyb 81#7Bo/<:q^!d] U ].k>xZEaͧ\e\ʵg&ěJ?|+6FD.A ^8Ԭ͚.b|RZR+&pHVḙ\<`ǙeɩĀ@8)JbbXq!t{D]U3 ZYyϕA9#I/0B6 ^#[oW9 j>*t IN>7{@he> ȵ)V'.'^פ `&RΔ?9)s,\Mu9-S?Xy,nj &PݫW!o\] sO.of C.S? d1LmȂHqu={Ld14BX1 ayfTdJ -ptsrp\)w\}Wib5hShes鬄EQl3aـC}! y:>y\^x|p(@!),N>'16ߍ2zp2n[S]ܗ8zь84!ƩMIwc/ojpA?b,mD XFڎ@gQjndg?yuGjuq +] Lz̕\T[,llUUT?{ u!|EU{l|7Z\rn}`?t̤ UR2[)9LR b8Cu)b.Br5 endstream endobj 349 0 obj << /Length 203 /Filter /FlateDecode >> stream xڕ=0!^&lj(m@M\ {s N`A&BZstJE*`IF`V^].uV R$3N1M}E;?ρQSxv[ KX%aoCKb/1RαF\ܜm0opKQv;ILL endstream endobj 354 0 obj << /Length 2255 /Filter /FlateDecode >> stream xڽYmsH_TuWVI%][TBl~u q֖s*14COL3Osx#^>pGN9,AH*V8BR|̹<)gr/ /()89bNB1˜*t\IHM9=/z>Q?ΐ\G)NXh<= o4&Sd~_.^1KeԚ\ bVS{tRoL-MWuՉ})ʤyVGi,ju m[y]+kOб6#VLmbԽZbkS\>GUTNn/)od$ONE. (&1.:D&vo V0K}>{>$$(@Ii$F[V_oklz2j4(7me_>_FQ T|Rxfn|i&c sBr$O*I+I H$,x\љU9 Ε}J4kSaޫ"[&TJDAywaudr%<֖>kWi*ʍ;Ͼh7]:צ Ƒi 97*JNlZK ?ƨ7x9\' cXErLHr^ >R8B![c!X$@+P$jُwH.Prcc#JXnp3ӵ] +'˞r9dZ.}M|}Ő?16h obh?֭ĸ|-HJx !Z1=o,T ʺyii^THt ާBɖ-`T w@`|3˗ d~/8Q~w8",8,ȨmΑ:=ga;N]>^u]_f䖓1ϣ?޾1scCpAq̭LM[ayF [++JjLJݛ9* (AW7E9=TLvb` _uyU_1 m>|}mU R,8l4=z]y&ᮈ]<Yv'%VlJUFg2YnAjҷ2 \Q}1L~> stream x[o6_3>X ?EH\$MS4=\>weIC~Ӳ6w.zHj8̐cޏG/Ώb0w~IP!w.^Jx/w8*ɳS&N㨌EΏ~?"ȑ}0b>Y z;/=x#6F{ H\!a8`i0XyNE-`\ˍ)X᝝Ƌϛ4M>~c&+VmWI7NOzAowעoI [ABsd ^+ `"՝'t2(SVwz$:/ ɯNX@*Z)xW&ٵ}ֱm<iQ_x|[~ nf2%||4"p>X(|]24;af`3Ђѣv6h`ZS`.>L_{ AR$ "6*M\}ŞOT'OL-|"YiI~sǫX*wӋ8ܫi0r't kl3w X_ 7v1EYS B,ЉpZ׫,mLM2ћE eҮt(?eG8X$t]~p^{wϒu;hY~6Խps/jwde״X "ǖ`E*@ avk> ;tW"@D6LI_<B (G@Z/$yq$c\YzjNxy OkǸ(!X>;wll@vώ=XKB꬯M7z2ޛhBr_vDŇ9*jANn!m'C{7Ixx\'2ڃؾ4Fd.]v !{ [}o=UBvI?[$t~DJ %DeaxO4'4E\c wS! aЀC\Zo7egSUr+l^=܏4 )j.@ˆ `h6؇w!h B)2a?&rq :-{X|\vpm9)^K)=S$C6=0ǼUF_.`!+*!(J/fbijgRÑ<># c[d^ 3&U7ЯN7C_룼A#[ʹ G!_+^lviWWi'pyHhEԃ-wd 8ּWFgKk3gcZe}ct*T(>nDBv6o.|6~teo8'6ʗ1-%T8TH8 0y˳1$ os `>käšH[w3vX5}e}~!i:wh?l0gu*\)~Jh,MiB}nF;RSa$UԂo"bu#ujU`pR3c7)/>l ŋ k_\W"Sa٭gh%3toŅ"̐]b%je`4mu,rd_hQ0"иt;дb$E/[@*(}MXCk]*A+PB'\РƼt2#ce׻:CJ6;/tRh¹ kG dĦf#܀u&E@Q{ҷ([c[9  ? IgTC":ĬULG7ɪ ơש3ʘ`L.b[ v9@HvnzKctͮyQEu^$zrѠގn@.~r]0?]6/SEr05rC-zT0~-ʋx`9 ^P+e^ה @< ױ ee~ |=ʜnG09 zNad 6=(]Rs Z]Tgs$542 [[37t*E zAMEvmX;}[YMx0Le{0rUU&]ȩ}+#η. J?hXѦOΖJVSްMlF %(k.ðj7ƃ@u1S^fhv:Fÿ5^j2G0p #āpcC]|v;R-Ha>jGlF]Ʀ. endstream endobj 367 0 obj << /Length 2273 /Filter /FlateDecode >> stream xZ[s۸~`=;Sj&q!xٱ&;CMO%B6[TyI")YDm3!P~>:u~ꍈD># J'B"B%Gݯ?y-Zu\E ޫLŕNoݻw` upr}w$0ΡDDR$9|3c-JAR~L+)=%^o2ke8R'Ȳb%y}ffcD e`K[@ 2%?/pdPy+q,MXn;|xu:)nPvȣv8רrRnC׼*]d枈ܷ+~*|1p>]xm*+GU ΜSx6ז[_ڔ ,r p'<3NODx]*\\7תtEIJ|2ZBó8CO'7q #{'0xCNXws ;Dö{ GI#2Ԫ>:$ D_ֱ `rju`:5fYeLҕJHF9A0fà3}O/NEDNfNy b ( fvzӫ誉|kSu~&#NZ'(ӅVQowAA988~Z#IV1hw_'#TwD -u3-g8ۙ$[kY4Ot>!c3ccU4&a4e*DhOQm{JYk +*}AvZQM۞rOJdW/s#1h0JFH4Za(OXб q%[{FX`+(  uBӈIm,xPZA>d^ڳb,Pwo#KKPZd?cJ,4qƩ/@u\kDVU` eס@$ᇗ;zyl845x< hkXWOγld:etY !%uIG da'▣u< /WV}6:߳Y;Q>g >`9B{?GS"_maHd>?{؞T=5I;C aRs&KI{͉t#1.H V니N[{OM;EY 'b$WؚE\rPU`-j==W /!P8GcإÈrf`@ 0`MM FÑ3"~j=^ pvDO ]^+4gK,g9]].e0##R΂y"cl9 E@ \ʋ2̂>;bI1wO\9vzhpn"va 0\pNw&Dө*lu2ݻ>UB~f hh(T.@?Tho)Gԧ_z.7s~ཱུy$QWX,d5 endstream endobj 373 0 obj << /Length 1935 /Filter /FlateDecode >> stream x[[o6~ϯP>8k"dXX ,͞"Pl9&K$ËlIQ.$1E|FQ;~;<M!EHNvN4`/4Bޔ=?LS P·~:qӮ}ۆ`3>~3Cps).>C67p-*da:L<zˇ 1Angꌀ_`WGv%BvGwF SDmw,ET FD R20 ۮw"ΐP#Xg0(wkսdO$b-uN(I*^pDrL1v4-h/*M@!`EFF|l-MZ>7&wqxd^>F^<*6Si6ѹ)xA]φa \ fK]^ l2?Z(N"-QY^㫉A[Y^FsLDI2͹"{iKCBF<p$0PDoT]"׳"8oVAEy$ZgϺ]"KlrO?]P"K>ǿz'H H!9q`o{ګH2Zhj;1{+gQۮg~kމtBW@p\…I&05$ב`KN4sg.RU)GJҺ&QcТ;x6G[~N1"@BBDkznBS"ii܇OB־0pl~l\7Mp#SC&vbqFߖؚ@WKZ'Mӻ:#dց޻]u8"S݃iS봩uN$$ Ѻ&1D;s1"~{޵ꬹJJqy3"ޚ i{[  Qh@u*13wjx~?(}'ds[_|ۧxSUU7 #c/$еkK.lm;K;a9;o ҩWv16dEmOH`L@SE.eTKXr&wê񪚰qa:y-%{H7Bgz>'U.mJc{.q!ۊKXPtHE _>(twt:)"ġ L:ys$ (2'ܡ"Esk.<6j& endstream endobj 377 0 obj << /Length 2485 /Filter /FlateDecode >> stream xn6=_`5#"E-fˢih Cسe=jy&;(:B $0,_AmP8O)dj S&h: TD̨OOPҦY j<ѩiå}/1l'վf0e8UEQZ0FP̐[sbySOLY-'MDgGnfz1zW\M4anuӳ3LjbW}f_\/0FFUM،S`SE*I-"7U)J!W,7 עaF|%)8N\b4f7<}XivEM+@!.7=m,|(GU W1!pFoO+ C|a0xhs*CXe9" aQlAmLx+Vq$/ L5F"Yn?}7_LP j.fefi0Da_˓a‹#J ;=Pކ;+-k[=.ڠ B>+Q T.[C .)rpRa8h"+mB >@h3\;r[$[ל=ro2Z|uX !ާՍ%MИlB+{ ঍$ky4/f![u]fR,F1u;[Fb`Xg`96]XtX w~7pMf "T7PMNҋ w  lkr^_+rkU Rí1r%1s!/.f,p=::1+1j.0s⛋0\{=8{ $jS]& -c)[ȮT"bw.^Ȱf EL)$'6}&/t s n.Td#TFʶN'o^Cz{,JZ%(']oxfg1}VqZ/!V֢CXˤ wBxx,1VOכzjoRDKӾEE#p3Hz6Jx٢?^Lẛ?yx]R<[$x~,{yNZd2f\i-,GCRwaGzQϿ9`$vg{ڣvTfUSD}Q’!A7,ٙ&B} v=U?hۊ6tenUKV_M)O\.DĿIolkvoo+l?K+>nuKګ$z-~Y^ m]/ Nr*)a vP#wζ)lvcA^Uy6 UNX_nv],Y|[U '5 F8@2"S@?#˪S0 ht@ҰABy<*ӪfTեjaH -1)>Tjv60Wyfߦi#7g<=ʥ*MꖋW+^` ?k*%+z 8'wM1$y>=c":Z -p]^6u ihP:?O$B3~ia+ O׆u[_{Ɔ6} 1.!#P>ȸ|X7. #D+M)%%j;%IkC#~#t6 c`@lLWX u "A 96YX> K=&D*a&^y?H]r\n;+V @ @NI6œ,LLIay؎P+㬸nl{&[G1`^}7> stream x\mo6_`5MR^E8]dl3:Kr&w%ْTXŸ=#,HXzz=jOdC2RU]K4CG.KszzQMN`mC>itZ_Z(` Mڭ4S #Хo[0\l«2VW QD 9at1p#t}%L<]xn䏼KXδtÄB'o95*pI1COĿn>ԴPO&i-ra CxkTBYT@T^%WV03ZK90.&3|IRY{lE2Cx1k=urSZ0ُUzHa1%*ȉi$(oԮ,YCHA_J5x3Rc-DA:HLUR ©]'ӟmb%~*`dĘWqg?lzq,?:;; px1;?RNMh\\꠰a wg^ *hS ѽW2c&lTu[w+ئ ^R\}{88:3O|w(ёNy(pQ)aAȒ^P\Ge|m 8!Q8 `4%dCN2JgQ ì5)GI I'eB\Q ~@&HE0 AxO4qg,pjt^,X;-5V[p·:ndlr< 66yXVҖᚋ$hz9 p!^{%  3-G>R̽@CF&)W*a v4)$eK=C!7:'syu -XuEzТa_sm݄^,;fWHg0=0<ƐLj0ᘧ10TنtuuWƄ2șP&x=''+EI–S%BA K~ɀ-OPH#-gedY.(Iն*{(\k:u'tJF0ux%@{E4+ĶԞPwX( r)LVkkkΫG^>[2X֫6t E'ejB* U#^5^=,}L1."#,g:F1[,j|A+M0蜿כ7gIUAend!6؆8C  fNʗTD󿪊4ief.1YVY vdEy;]| X(E7 v?bە*gsQ. [3o\smEנmN %Fby]٩ye+aI_XL}{yﴞOrⱌ 纸/%#i]?0M4?{o=1) 8,&[y\Ar*oUΪ_[pN_nօ+yU iINNFjPmXȋwRS.PݾNU*:lμ6G""ZJFp`V%JfLG0N!of;@*YWM5 )Z,weZOO!WBM |wsԷDtO[S;`u`#ۧBI/L5ng'ks[^z]r[xH/)^0yy+xgZ?S[ k'z'cΛ[sZ@FݞJih]ڥc:?v=˜>$ endstream endobj 386 0 obj << /Length 3234 /Filter /FlateDecode >> stream xZoB a|AI$0]ګܮѣ>jϏzx)P<xٗgoU,e\*RL(HR͔V:q߾b)Sjwh&/7lMVz _|Wg X"H$P Y&jw#Ao ^z(1Kh{SU('r(^ D;Mlt?}Ve44;5 侸_}'!7̪m wv5l]5p27`T.8SSS^ w07WzdGKd!$d`)&ܬv ,K=@!$K?uyx< x D7f.+CTS6q7g&AL$*!.b7&/8mYA,U  Dm^ ^o}Ǎv$ BL]޺V% eG+ew?(ܻzoV%ϑS'&w{kC!d)^|e:r2 m.cZ4U~{!G^/ORF @>תW:_#2oǏn.TmkMX0t IC1gƱSΌMrMnԶ:ǁ=I PD-;AePK vS`=z 62d"LLuF6TfucrrND;<q{|"vHh- yA p5>؎oJb:eԦlL ڞR,SzD/vl$Y{'E%#5@;\tZ}T"`2 hnu,cZ3kr5 CE (U3uC֗eVkuhӬ6fgڪ240ZMִ=p[Dn8NvczzW.B˺o=|:B:aBiWJ@ceSIRxBj&%u̕[$|Ti2 *h!)/Ή@~ m 3$c))4\Q>ԑŏ3( LKHOZ{(g>)#kbd.PO<3~Fh͢DĞ?Z=Z\hi22/x?vC2;Y}BSy@Fbdc@2[6mE-BWZ#W/8"!ȿiVK7*XR$ L0[QE @iphDZ-6;jզᒳOq. WPr1|`A*DK&, N~ܯ>s!ˀn5Wu寃Jcpdǡ;c6BNIEf.4dC"(&moq3-n=*Hy:M#/l9`ζE_8rۗ~Kۗ.}QW~dhxÍhN}HR)?s@Aba:A/zd1zYmD2X^bΈߐ y 򦱩,Qj_FwD2h ^٢8[PǂVT{,׏x2t1Ӈ! ɶ]oiwI&Xeo}7(": q5pˆ[w/3eK"4n 7OͶ,m=Z-/rծ֓p+ä74E5t䆽2Xޅa=l2v<{<^ABqg\օbκKk{0!˷٭ 3DYVSl1:Q'W& f ?KCL#q[iΙ>r?N:us>O1Ş~`/Sy J&D@]b)mȠ[ NY{b- endstream endobj 391 0 obj << /Length 3193 /Filter /FlateDecode >> stream xo۸` fDRC{vVlC!t-$iG}ZIc+msP(}or3&?>z#I"-z5 $B&C9^N~3?g֤e\&YzIwfcЃ`g?\vaoiE(,g?M;ɭ3ho&:0fBw?º 44yOTbp_L(9`[3Y 5h~0߇u!qu0 گ|L{ sμst-OofYP]C `7qE*rw?7G nxkJ8){+ UR;v t4&b]:u_$|xnqz%v@WjcI"ƃpRÅPx,} ^SFŇZ5o bz!/@4_Ȁ@Ւ_ k݈a#Dozd+cp[d2/a|Z*&l$v_&.GyЅ107nDAen݇4v ;$IS⼚TTˀQ0)&ըoIT)E<~0?XLS] K]@~񘾘 8N/gr6xQ|(oFޅ--<܏^oͼ"v1݀ŕ;wCPA7UAhݶ@ϸc u͎^ٱuApI1T+EMāii4*٘ ՠ~N0o]}`\{޾zM@-W%p*i kث,"vٲBg>Z%iRMNUkNߣ7v`^$h7i}rjesSy|r5?OVъ{:mAt=>^QX E|~iPN ،.)'ϒurB~!.}ttzRgFcg3:W祏N&X4'xӝU\.^1U_BN% <,qy%0ZB2u,]1Ļ"6k6  A+I@Kk2faͩLpVnUȸ8j9@U0PmGN?4HѼlpǩX?ꂐ~/Q"yR ؉)SۻUxΈ{xԣ[|cV䝴WmBBQM d1ߦ=>Lj9}ԩaT,- `\gK V\VR't~;jʈ~@*^$zt^ ' (#:5u%% &П:JhjFw&.Jo$xND` lkdiɹְ*GaJ@s7Y-, >keohp"|LGkrjPNo ֦IEyYʄcvvfC;x@Ÿ雙;>M~߃|[g$N8]; _@ȘF k-`1;T=\OnLj/To4;/\Q_P^S^WCFK \&^{61,h}8B}7JB$F ڥFXd1qN1ڙ>"⾕v15]Nj߸f6CeP 5rOz6)(p u6D$kgmCa0.uvc [m:m !jDLjOshs-+<㴘iJK JjI@/,r_fyoj2 endstream endobj 397 0 obj << /Length 2477 /Filter /FlateDecode >> stream xkoFc􃍖}pw"v6#8~H VT7STb Xrfvv޳ޝO>E^"Awga(D,d{}w}*óglViI~e^ e(?_<>OR Io>y0ÈEwoVD?OpL[djx^,8>˷Ke*-q}|m`B^I|ܲlf[КyvGأ;0CD cHo.#ELx]PSIy!0I0 Sˤs_`|#+=XIޭWfCH(D!;b\ϞpS{ CÒ"rPPy*FϥdPq>[" Ig܉J7ER}ȶnITUI%vg =H3 o(U}4Y>%sϔ#*v&t>TU(U8'dC =! ~2X @HĘ K;Wdku"%A0 Q(!0ϳ@)4:@?R-N-QӈSIo`~+K.PT2;qA_BD4R`B0zD l68 vFZ ɖ6+Fsު#zy {j `//O_]\uθG@;T$4iٞtcCCkss0l,\y|2KyhQc)g Ǣ @*UIr"se7j ?k@D{\GR 0ywdGÀ~gnp`ʕ)Iဦ"<^}p>q@D0t/ճ7}{@P@pU\7ePb>B8SWWlv vCJtQm 5eE&]TԅۦU{x24Q`ql##DP;jKl-a .T88L_P@$xd$̪2)Dn&zs}{Y^1R8vf^;<9 S'#^6'mK!7ܸɷc&DM8|4XIk;qd>W]2Iڊzԉ lmjVk.ZW||JLhl?!;X@n:E`T9U}%7ad54A5(B DcxKZe7Ey`3ǡ~2W/ծ=\@R1w}.I}t=J00w-aهd'UX),c,_?ENXBWo?99 t~ݨGLէn+f>cCp%Q?7Ŏt5'H4)*`BuRΑ~X_RH+wyvC'[f2 f:s_~F;:!tz-l\t\zukz}F1#Hs~EqIS7@KCcAB׺^lF$ && $peNM>#]$b*/flM `^ʍX} h+|fS{S`T2eIbTpq4p$.Wda?Mk ;;U]͆8"vhWª0zB궍 *w<61]кaJ9+: ׄcoaf}ҩ{39D̵]:Xjt|0dHtL~}VY>2&MWˡoyhUdɧ3],gsdz?uTU8TuiW-<@TBv6/(׏BI endstream endobj 403 0 obj << /Length 2691 /Filter /FlateDecode >> stream xZm6BY)HR$m&8V%W/~Edy]7!@DQpf8 zkz?\xƏĒKj ]-w_/x;/v&irʼp]x}u ŀ8Ňߨw%~yz #<0ν_9bġWnpAgۼ1TleEL=}#)I,"BhY3؉SjCVv01MR,P?3YQgK[ D}fY;>nGJynfynFz Z=38 3f09386|pt8p{ KR k;S'HT2p郐~L/$ܖUT7jjC"מ:H.ICؐ$~'.@~7bX_4i)MCֹjRŔn!x”K"dLc3tk,="c 0Nn)CV {B?Kf?۶1dYɵv@YդcNء9u0d& v dZ{Ԃ189bZVOb6`[00 z,4VmQd'dcGjduH.ު4CK4$G`\֙7 N}IR@D}⅁pzj6PHfb@衵hO94,Ps`TǂQ5lxQ*L$ z/ka@u)4H@ssqيXUr&!Gdw&Ko0L249潙LA48!Eb *[MH_OB3ٺ(Mxg/&p-e(Ng ) Ўەeǥ̦Ԧl䍪 0O@CƱ¡:ĆmU.T bmUi"X9P1KV#Az/ͳ:U_"hTI Iܚkձ},\/ȀK+& VDb.i~Q#m r઻)^!?f?a^aw5i.gwH9Tm{ K9cg0aEH X ! :0? S~梾 !33I4$6އs4+<òژDV,VEc&Un6IO݆#5Hf^©zKZę23 H5#rgzmoi:XV-bUsS(:Sԭɬ~g6 #@V !GGO\4O*2+3); jJ*|w&cESws;t/gʗIe7I' į +91DVxV(+LU*m*Ó 5|,=SB I^ݙ&)ֺ&GPk-oMX`9~l#37>LLƭ@)96E_q B:c7t/GGpEIb -ؤJƢ~# uF 6ğUy]ݨ $N>;TRg`o V6t}Ks߹۟_ՏS}+zuH57ɘVX!n܏I>[( w7:oΛ`h!fv)gbmZlѩ.eEvj~!ov!>:p"uB̝m3k#DZmZ0欿P68!'BFJ\m9|5Z9 bU{WkoŲӛXBk-A&0I yx$9-BB5&²4#98&(Qktl%X'wFXf f:^p&hug SW6m2S~.:6泏ݒeء2-A [0XA#kg ǡRbZ1#' \|S ft$^ KAW~=k~/y~tMܴWo/+ Kz(;>92U ,gtO~Ibdr /;rn7px#ו]u!]#f `OvEфL4m#ő][w==M]E^piY= c0l˞\i 3a8j2ѳ6(%_10HH_e*I!Kɞ<1b9f \sqLs" \􇂓p|cW{wRX%kb5'H endstream endobj 407 0 obj << /Length 2165 /Filter /FlateDecode >> stream xZ[۶~_.΃ D4-6i(N}Jlk5Jr^$K,bI9Lp8o޽^^^ȋP$nW 1,  wOԧA8._6**ɳgV*.A|޾zu{eGGDx_#ރ H޻]a'fHLϔ+~c#%;"1M19/ƓwilSS'M\|(;+^ R5|2T6`I,iO)|1Q3ejdeT nw%1M<&/ U=MJ;NRyY&-[9jY#mt1׻$EsZ:*ZAoȉV: >Ob') @pm"`^@P`V`wǬ@70}PIib70,8ĬUu(okf`k0l&l*JkK$qk?bO0NdT 'gb n,aS(KT#݉u(/k"AX_\&^=;w65z+vYy}ڄqVvR! %/|vE%'v S ka5BO9k3N#nO+#왎͜ʁI`CN`ӭEy{JA!<@  0$vG!ϱR".`q X@#jDM QFKOqf44Ţ$MOyؒ$]ZJjm/ զģNiL] P6F i\(D`@5pIו%UR\߭Fg;K@tg-X~RV*^֎vAĭZx4@X6!1!bفRwɒ:[`>qb [b7T x~a`{!ji1g}¹P-9@~T&z`TC(}ѵ0w-xֹm W32$U:ۢ{֓o4c)Q@߈h6.bASK!)bBHAr=/F73MFfٚeyն0+ﶿ Q%q:@  n׺a;ؘ>Aij] սe=D㋷5XH!WC[kkpv5.] ۵.jw @3$cF ]P>iISHyWOb$=A~]MèQ|5k ".Ʃm`xYOXo %s`y Q!@K!% ƺ\b!Y}D8KֶwKV\r.ߨ*(SInIN-UUl q.E\),mWPC|_/ZSWCžu*,&:=q. >kΥvpd~Pzu>%ն<]wxЂanr7qRvfOGB"OvuSK "M/CR~!'r9ؖA<#iwH&şM5ϿdmI;s[Y~q$؁L&F|cUj…ƾȊ2r9uhB{C)%.H W_ٙӷyJw.dGm\UT5s}3@B!CLS vn3WE`W}\8Ɇ"/&J9^!`!vv1 ~j DM&рw4f1QKϗ,ĠG湛/شj_6վۂZZР%:Wםj](wdOi@GXv.qo4Geo(Rp#~?Vc^mf,ۨ\<\r c,Cڔ}Jʋި?~_߀BBhV> stream xo  .wGE"vAn &OHN݋/Qv$zmx$V?wNh(g,-Z~ 'pzgzlNK6<:-/5xTquA~yuvus3EDзٻ_+ #' {yjc {ά7g?aM!˕\;j1bB ABGP ]# Q@pi@C.w, @+dI <\苤2jAS&`(н p\J#!<-V@s:`D:!n@9C8gsc;ʲETѳyR\͑5'|XUI0-&*ߟ BmwƌxB=C>(mA0!11cpAnw!cp>r<$~lajUZ{B3m]+mIhGs=j̤9u(w5GÖSRkc.u;R ꨬ2ZgR.U'\Hg #]k[T!i;թN\BSR">țt}m8XW<[!crAܞx=36Ttc-Ѓ_i8%<+pe1v|-[B}+6Y.H:P~J`) Xw"GeQn۩3z7T">#㈋|vhЇM*С=s}7YIr0ͣPBqΘiYh#t=@2jm%^\ !Z~Hq]D&n.Q.ˌ7R=\!CvS3YŕԪ}*$͐$ӦO RZ4")F} 0 Z0Ymy.x.u#4SE='M602v}bҒ N,Dh矺tAM|ValD{;@W1Mz r9t:q_n}9uN>H/9pI͎L3uD/ Mi4W"}'i~( (M:L3+Nmu<ōzon4scc¹QQn>p /4 Yշn &YV>Cy74-^@ES0A;$%]?^,vurD&>-p[۫SC AIs W]IVQ@3^%J٭g Sb` f0_L!(ѩBW_/~~y}S  hh2iہ]Tn ]$=7xϞ5V #d[{ L]g0FN'N?9z*H]*9xNN+>d.Q_B~ l]#yQuU3?F2;1=Xn2Auv endstream endobj 416 0 obj << /Length 2498 /Filter /FlateDecode >> stream x[mo_Ar !ik+n`PmHI )l Z.3ϼ;wv~8ywur|խC Gbsf/.Fc*_zyGI|f.Edy>]}8:4!@#Kg<p0bQK tp>[6~9qU4Z{6>~8+T7QBtй9\p`X`٩\Z1r%P!A|Gr cspuvخ,L7‡U<}i~ RHֹmf#$a.1 j71g ÑuUa4 $yUy>2-}##L  yΔXZ /U绾j"K5cbQ@2_$tm\3YZJ+PeTm6\P8 $!2`H(.$߃&BT]F?ėa`aSH2 WH@)-6&2;>瑝*#W_j j!f -Xid>6JXe;h4fXkB0rU%toA˸ˈaFdQ0P:A;7T" 0! !*AJ o0ز- ;hfF,n>-< Y"(dkGshѷYxsKdFjEWQD>R$qQ|2!h'[fp2iKy)57ôma`b$!; b0;Tlf̵AAW_ B7;jB-266S"xCXz ee,Wд# ,u/QΰZD(_MjK0%H,u#ALXeb,1]57LDmAn(ijՇyԡȓڋh͸ɣD#—)F RQs))ʻTb4SN%&iv &dlJ1a!P5Mu{OSfPT Rf4\ ,Ib?/fs*uQ]%!h:7&,Q(/(Zj6?LbNεAG 0ƌfh$kx~|Vp-[*ۜc k-GdhJ.SM{;y +2/xmڸ XC L5ѯ~qּA@+@`u<2sJ ISm,:N8xآ b븼#Fy ܔ`$CNpC%r^N' gg8 ߐɒ?L}d=]~g2h^G*> stream xZo6_g2")#HRh#uч0]Wq gi!0`#rf8+ov3fߟ}{yg.׳P8:ҳbhbڼNȟlRY(]?3 $0q8[n~͛~ G[7j;* ~<יИ*$ /7(x4_fw~MyklS^I_~X7d չ9l,Vi~wlVjX>4QO7[HO&f_G󄉏WdrH|wIlm +2)6BK}=9}h0|ew6_]9ծ@/،.U,lO[X GךT)atږݑӼ>}/>-(=yD8vCEm[ɍX`7N"AžwŚI}t$q9ȵ+.y2[`W}kܤhO苖ߦ`Vq`$-Xv _By%{u^_(o-Z2d2l1EΉk~ʹiÀcWŮ,FVk.N5ͪ]}k?b.KvIOjFiɫQZx5\ےNhky;0HuO>+z!7@q{IC0- cAg@k>/=s&[RRC(Ԅ2:paMvuC}b% 24f@RLBr͚DqB!GfBcko4 Oȁ% |#9t1)ӆNO8<=&24pRtZi Z3!V^%[:9)&)-=s_׍ܖs\=VO}ok ܻBumEIVא~180ZqbnwWتbhUűCmC\Zh(nP`($gr 5MSZ-0bN6bhZy~|)@QJqXb = KvE[lyL=CWnDp`%g%(-2\𜨇W291C%Ys͞q"gǰȳ!면cf.}96 l4daS}gS*v#IY1,Iy:)ݵG)! svx+v>'2>#D= S>MG9xKC1tpuÁ7Ȟhr 2jp(xY ^8o"?"aL0<ǯly!2iriZ\gMtҰAKV4,q#QxV;,MEC7FR$];'q1t1qKV%=M6&|zۿⴆ#ڈWx?_9HVMuT42򂟭E`twcrjB mO//Bp\@c32>R k.(h)oݏiUM4Vsل.}? Y h ۲|Q@ݶq|W\ܚA~ ]gfvYSD:ɓ쎯jX CR7&niy Dsה̭]5l i6>w=&R.]aAs 4(< I|-t%V^$jO U:KG >Uzk K2ɇpMx@`ݏEV]uw]*{K]i~c(4[cE|#e eN\68,G>Tr@5p~A:b_zs.W>?I@R򁁊 h9eoOwNd8Ae;$ݽQCTxa6m\C!?w-Y,͙]mӛ=c,ODS?57]:|IjS#Tr;g&mlQO~R;` 5IvPR7+]BIgE&`a+ endstream endobj 425 0 obj << /Length 2093 /Filter /FlateDecode >> stream x[[o~ xCN6YlP4>m-,'g.$E-h0`o}όpp/O߱(P$ . 1,,\plYtZeg"qɿOL( H`X]ATIho'?Ll7k|p`O;&$`Ba(  ` I cuI5d Lb".,=ݞ^2 BFs`* (IbN:jg@q'3<;"*7GC8J'vjܥ\گR_}5S3!ԊxlDG` I ^-g?WdЎLՙ17Gs /v!Wx㍘I}P ¼96 XD .JLxm+cm8(Æl |3?y?EĻѐ2׹M,oE퍆z<62%$"6U(V'(C=xT5[gX"^\c auUl$Hö^n2b{B0(l0h7 %&!"RhbP1֠{o1K:stt޳{e'\__bK |VlD?8r=YȔ`C[Zv QH}[v>s[v-<:&!@I_; C plI@4*BBp'ˍ/8%3㹶+H`*Pݵǔ\mյM0Z>M!g:d&!\lW!BQ덅>&`$ .*dXar! X ѬA6ukdkA_% uZ[4o9ųOs ][uAys-/7] Î~ǹp\luJқ l512޺۬*UYxd7:ja9HDF!1.2 | M'Ln]8:{L QY2'5R Pg!'M5Ob?6 XK}SmW00ޗU]-ֿS&;]xz2uSç'vvͶUtY B^ۦ㓱x[Xby:2uw+De\[~LW843񰮺:@p.2?d8}\SO+ڏY 8Ɠ¹a̻Ay/Yu&@bLoGs]d=Vn0svh9*]lnt~eJ& QyHDxJrcB "-RiŵʎgII{J3&v`Ko͵dǔ'Ep{^snm݈Lʵ֙WGilöƻF Z׃ԏO" kmFËb3z%)CZv<ƿ٘RݔϝR$?"E@BG`\)iL'3o?|ХxllWߝk$4%): 5[_;UQyOU`8ERTRz_,L!jV::G endstream endobj 430 0 obj << /Length 2344 /Filter /FlateDecode >> stream x[[o6~P>H] Efh;#R@^4zImlERG;xޭ.^_\}"/Bһ^{(D,du B_ީ4ϾuVť7߮_7`X(V_~^=Xzw H(o.}ش-|Pc 5e0!  hcaP^z,4& ӯ)C/7I1$HKkoqTRUoNy t\P":ye)THfj@ (:2@.i#2qHx:-Fkj aTN65PDo1eQ$"T 3dYCQ耜q+c!YUP ?CSce] j8Y m@SPiY٭N7sfAEpEI~~ЉgQ_/b-JWI>x*9/7jc\]%/fv_i?R,Gګ, D0,CX>cb9fCa8R|VMzфx`;j1PC'TMX[M5DZk8B!asx BAAY?Փ"AդBH ̼j\%1wCBijCU-R? 'pCW0!=)TYoQC~c8Cy #Lo#f@HnTr;@Mgk;%&uY=l'y_zxlH!m!^*򦿊X$k0x$w4iwOZIHa!&yM2n" [< sG8f gM <c2B42$sđWv>V]\>U"1Nwc'pnO["DH¼@ `OBK#Z"ps!;0a s?f ڤ.a_]ǻV&V1LpFlE\V MMەE#мAQRIW Rتɧ 8`!Qcz[yX \pjVF ؿM/0 gc%b/ژz n}XeN@S k=kG0YE咢s퐜MHrW[m0[8fN#F^NSZe:H̗hܢ;{׺KmŠΆ`¬l=rf^q/]0tg0vVu :Ԧ\44~M, fԘ ?e|bx Dl1ǁ Ԅ"coljXU?O$ kZեb O<{=F۾ev kcO{& Ҫ#(#⼛ 㤊p95" |H{Q9݅e;gĝP'Z=Hue =<`32 { JzO lГ>iK'=09؟R'?>,AY|pwaΆ}EkIx!B$?Uxzmu<=G۲E6ky#*OAOu(@\HC G%v #}OJClevH!z ?` endstream endobj 434 0 obj << /Length 2138 /Filter /FlateDecode >> stream x[o6~_kE-Z[t.MEt-PdKv hj8o8oh!/g?ޞ]~`:HK*yC ( 1ŢYiXM~dUR-k1K=P~<3D1 Gӟ8A#U\ZE\Dc 跳a&?/"ws ȵnlGaB"v.bDHc&à$XKrk-;-*ɒe]af"1bJp%yk!lI3ێ)aH3A=9X3o]aNk4_Pq Ȯ)}[LYGZV[n -Κ;ևϿ똶`$ S YhJ$"pB }C(  tA#O |~3YX#J AkC|! }6>@_}+@P@‘*.|%;r@_|wOBc A~}JTTĉ6w2o[-Dm!1$(bĹr*?~@mdCXmk1䔣7:1+u.X#\ |#@)#'-ս=us`0![Tk9W ?%O?x~?>釲J_=YўͩYqPDb 2)=K"Ea`YDEY[tf{˸X3h,CQOSV;hL/|I/(,MG!vN4q4OIf I>+HxLϋ'93TL]۳0/I<KqIZe]hZmL"AIWQ UJ'w$qM34lv:v &hK3FAS-'`LtCli!S6$UU,-TpqLtUYMkҀ0p zCз̏.CjY+S)Eh56Śq'RQ[B=|zKҷ=n<_$yc!A.Ȥ 4n¦!,𶣳{XTY}eh4qd3) G^qwf aa'~5{ymjpg?cTB!wYr',R~ٚ@`T6V;jYPpg/> stream xZKo7W@0Z Hrhk&B4oJVZv{H8 QHP L>JȵaԐCKlɶaijeЪ` &L!7p͜0aWj Y- c(w9dr(QR lII] >@5H$\ӊ9[VhЧf\:k+5Cwq WsjvS h[ N H~ u2THWƪE1Ph \T{ۙXP` la5A!H%$Tvw5:PǠXrj.g*' jQr,T Ćdb!E\w Uc؄)(|\=E! |qʾExU7uB\d M  %2r9 Vä&3$p;&@b,8a˯A֨_/=wt"xR+%zL~pr84f)`IC50nTސu8 Nu﫿?b2.`7.Du?zS8sjOq^cKϺ"ܽ_,_~{_eCzaY\7PQ&:XZ"{,5T@3˰~jofEϣ֣#?_o+a;~rdS]h[d`bɞ!2]󈘺=Cv< mfcмm;"*QyDG]sјsWD/i @RFt[Ge\lKɪJ,#C-Qwoj/i||J}BE%h=AX`A Pb M3#׻&FP&ͩ"f7GNt2wB]&Kٌm3򬡇wnbjLnw,3 JUER0ט> cN9b@g5=Ou9!рbs(2Qp'Y˕ n-eqQ5#d{4g.tNHig8mi?hÎOD3w 3C'fe:@a~_""h/n@:=:(*~ܺR!m*@iMq i[8mmMfPzAZK%Qk'zmdiÖO \8X_i\ӧC=*8W'"N:Vj-i-sKu9¨##tf5iͿӰg;Y[` ?rxSHsOkF}8ړ2$O<VB BK駃Ц6զmMgE.S:;@BŲ>f  }E`TsJU9qV}s\nw+\ߜDqvtp+|;.}\0=.Ve endstream endobj 438 0 obj << /Length 218 /Filter /FlateDecode >> stream xڕ=O1 ęĹK.#t4LU.@'jyzG+Hx`+Ϯΐuj-*-[~>cisڳ 88 Ķ; C;] ߧ F!Y6˯̿ ©$C-x(LEe TÿI>ei9x|pO endstream endobj 445 0 obj << /Length 2123 /Filter /FlateDecode >> stream xZmoF_A fߗ,ɝ&-4vPܠ$Y"U+7DZ#ɗw33Ͼ2Gԗ1(i)\{u@IRFC}|K4$FU#|)qpwߜ?mə$Bj(j$bO,yG>jb"B@kAx|{s޻3N{ {Ͼ?zW?xf嫍H”p-?.[o==nXf;㋁/h/.YR%:b^xRY YH"0藾J37e*\H'h!Df/@E{8sq+/}coγM̲r2KBHM0B!`)i|Ki5=o!>4[*awj]"j9ޢZ(4|+.`E'/W0@p:e]j^fK?-|J~@OCZIq&,* q ҃$s*J+tv_ ˇЏn|̇ Y>sԹ 5`m}_|W$Jr 78z} gw|ڶ:ꀶZC_+,H$C f,+{ݟlx~)F0o_~kؓԿ|a4+ކ|/Yn͆U38\ gNdsWUu,|Gx㝈$4JC"@2bYP,Fx5C`AЋ13_e_ˬm;~Kf7ۂKC†nWkGfj\-c:BC?v\ >j⸻S AlMom} 3'%V;mrb${EG(E\980 ;6@ [ ]NݬT+st:$k"'MԆ^w7m+gj[?Pz^ >EȺ0_= -EX\}+@B`alqd endstream endobj 450 0 obj << /Length 2707 /Filter /FlateDecode >> stream xZmo_ E+|+.p; @ݭtyY,N-2 ??:y%/aw~E%1W{/߿(()ս5IkC/!o篏^}:0Cq(J"o9z[ୠ0޵,4sћiʣA.A &kuJpt"P*bLX̅W-bɂA_&Uo\V^ ղ,l{Z7fk  $wTrA`(Vdnw  c:xʪMMES?_Xbޤ umU\f⭟ON^%d /i&Lho %gnCF(4?Z}[ݥ),g#q{2c4SϹfH .lo_n= :#]!#h/E^ 5!2@ ՑV, ԗ#W̠GpB&"atVju#d_ꬸYrg~Ag)D7SE"Fs @yYEnj:s*Muڔ\V&m Sze՘ἛeSV7vfWzcB%bNUבjU^VSE`AjAeZ̸o,<[RSkjȊ:[hjN+(HM2\Xfj6 ?]RrG=lh̒jNA-ւZ^v#nsa[ٚbZ3*3n[ yܚ2Ls6atYw=vi)+_XTtqt6M-f!q DuOJK26x$c&|-,4?jp0{:Cv'K'h좀.[zӐᾛ~8Dc^!z2RC HF-˸LMamQZx\ܺz!ck{ D]'`bAŽ&ijc4Ԃp_˅USP NPC\bPFP ~`$B0w7QC ࿳W6`)79(vlY\O,P$1lg1l/1@d 'uvF%9zY9mr7&4qgnrgd=4fN/L>1 TIİQ_$o!Ls!,X;ثAvj*`p&;>cR 9n!Uu) ɰMj =A.&3GLwc}N'd+zZ_6BqcK[Ț69m,F ) ob)hN_i7 `.]3@O\c⦅ mNt ' Yk_pP+f"1gvL}aȕiݦeLaɀʅ^)k?RjSV[5rJM4f; ta[p7sv.P.$:kf&*бڵ )ִኂSB[Uz+6qr!0-f|laecx乀TNUȂߩ \uU8nOxg FZ\MjM [IZ+%h%z:SKTaoS,q.8Bgӓu n8?.4XFۻ2Mvuҧjɴ 2faǮva }VWMO| =EH rhJ "kG^#`94֒)!wwS<.ɔ3?=Q $ P:kLM/P*J6#ge6% +0hc fH GPETgӶX:l^+B2YQ;XBJ % SI =idEA(!fSRa@W emt`ԽfO%,A r{l9GmT9\fD`1H;Pm.Sx|taLxlanG~1n]{>:|7~)D:^upJPp%?R>ܔqvEvesRixՇ_l쇦CMk(R7ݮrAy1SCQuC2(U?φP4}*M`?"2V5{[e6:FjP.+*zM2(vݵ&3a^dEC8rXn25즻1::u&^ %#ua?;L!`ϣ.ڒRmJP/z*SgI1ؾOf!rn;ZXKq6y q_n;3{C1XE;_ɐqvR;Lmwg&=CUe>@?7;lS?] endstream endobj 454 0 obj << /Length 1937 /Filter /FlateDecode >> stream xZ[oܶ~$5;"-=Epd4t|u NE7Y.v훣QQނ!@xyqzwaI nQmvׯ,?C߯>g9@ Rnw+"H2/KS0!2A C$=.Xd+C@ఉG C$a)[/kzx?&S$NCؖ%j]sAqM{&Vx"gӯS~B:]eU}]bL Ӡ+q|ӛ*>e x]k6$`,pN ~|3Zxكiɭd߶>[vm7ڷIMB6IJ.jifn"5ca֦\nT sf z=ⴜ xG :0 ]]u~wIAK%lY6C8lx!FR) I ")?3~> {Xb5 '1Zo@,k.b^gO|Mϯi_/O@yj`s a' |+*_>=5MijWeVյZ[fS~ :ws ;貮V!fH[C<.v hEz瀸3[Q,m.Il}o=*D).VϪ%O>̈́4P*K]pԡjERns2oo6mD4ѳ+T[ >A?l&&3Ij;2?٧y0wm־uxXiQTg{9:++(*}bmfY\opNv@SdؘppZZ,1>͹7gFz pIEJUކ0^ &jdG<'ugQF19&2 0cl>Ɋ3ߍ x!2,MmK+1Qp nHČt#}vYK0L+>ن eCm # /rLTi17]JLCdQ}d]$8׸_.uUMٸܼrRts26(Y,6ا~WUJMZOɔ dEOr(þ`ZTWӲW{2K߬2H{Ð!x>bRQOVh {iHp^/ ͑r@gP&n$N7m 2. 81 ,3M3Ɵ#VY\XW-K_jCKmV~S6B2ipg0i}(ˤ /_:t=4K?8HJ7l{~râïX nڵUG&aDƌR4* endstream endobj 459 0 obj << /Length 222 /Filter /FlateDecode >> stream xڕn0 w=GHXi[Zu 2zv.(Uf(%?Q+XY`wKd!#pDU* lx<[Sr%vcC=\{md6Tl؉ɴÅwf+I *_em$)nቭdoTJISa$yVxL%s< -_w&1?o/כTSo endstream endobj 463 0 obj << /Length 1835 /Filter /FlateDecode >> stream xYnF}WQ,!7;VJ"5 J%6rQjٸj( Ù/v6v^as; <ʝ vְ( U9'dz/Y(T8[9"@X;'/^_>Loov! *(2էz6[ >=FDGhhԻ~|~9uL^g~lxr.-;&>c븈 Ǔ$M5+4[k:җ4)+MM)O$ilT:* 9x1;(b7.jIfUg%/S (ʕ2A G^AhJ/"D><r&3%d)b;fm*s x^㉻Uc>yBwAڡe!: ᡀl'Xx0*_cZ`,^IwCk |SIM\=t>dt\īS2Zm )1=R>qF}TIlwjeI3R/S(%BFh{_2`0 R}SL6.Ϊ,VqOI{Z[pE;e 1c1Je|Kc$yX'a o^H#]8dp('hRo6CgvL'qOj`UU$-S@Y}_1xw D{[}StWU dxʷ $XAI4ϺS;qf{tʸ 8hÅ,^0kx7쌶  9 #H閛bJ!uᐋ*7 037~!a KEaC,r̫,.q*;ʜc4-.Ȉ_MT6ȳa6[/, < ?a#XSC]zB]mYɪ>ĩ !G/Oam? ~H-G~P> aA! |gW€ T5z|<_-ꔦbd fp)7XɻèC f$gxu>1d:D : ; WJ +i$2GwSV(8E{0 oqB }xmDP?GQf endstream endobj 469 0 obj << /Length 1911 /Filter /FlateDecode >> stream xZo6Be b&ud]M@4X(THɖLc]EQp^ %87,bK*4P4PFL`7ϯFU:\Nɫʖ6sP3+8X-R`b<6Q%=~< {:R8pK4eݙ|2EW/s7{mn7oX[2' t98!!,b8,f1 t#"B!_ݣn#(VRDxiΓCD9I ̛-Wz黖iX I+[ѴEUrp]%D#=Xݸj{8}<]w5;knpu?fk7K %Up#uRk Fu#ۺGE\+eAY0F,5Qgf.HsDܯC/LiuDxx{E M]v(I^yKwt>. )9UKܨ>vL"{F$tVwۈX6"daTZVY>נԇ!Nj/|jFͮCM;NNSY&s{/C{hY0{^nP B $uTkFG޸=Q=Z$[$DZ})SjngieC)ѭL^m%/>EVyaWz=uzOK(n6^!,=yn#Pf˪;Ѹ+o޾R!Xeږ<)-p~~[꤈(p߇H-Y"C1@؅qI4l@l+9~&zg^e!AʯL`PV]<ygYj~ z\ckOK@`dl?A h).6իjX"JPj>RZTNۢmڷ_A']Zm]ٹ6+M#MvUV$Cy *JݤW 9Qe,Ux}fȵ338>ז-Q`G2GԿJN)Ll4°M#а'MARnRndY-d+ vMs5t<=uL.Mʹ!xΓ6MaDKZO$} s2Te({vE\kEE_JRk{^ю ]YXd0>غmI::A4? ~ft`[kw &OLZ6D!rh gm ;r]W_l +V^ 3vk> stream xZ[sF~Q1,͉4Myד!beJ߳7b)VҌf̲;{0<9{9>{^␆x1qXļq ݾ }*e1Y-d^'uVfFeRIsCwgg`xXx=Ra{o-DE3lmqè% #ۛѫ/7kܺ9o-<+Ҭ(gdfrsY")D*Cj^`ŃD ( `ghmsĻfA)+Pg0H1oV(Ғ~OTˤY)_]^>}٘2)O7xcW7:܌>^?]_'3e̷'/ڎ]0dM1BBQDNScHCQqRf2P|]:9M&@qD ?`½_͊!GK$O͍&GY>-ʅN=A|'̂=E_}  A0F!e%'H,0bnT L{`>dʘ_dnzt >`H1l|_o{@Baڪ&,f?su>, v:B"<JPl/[m4$dP<>)쓯l&"JoTEE&"iR˭w}5g!qkz*U<1;nW6@PA.`LQ8X7+P|PԲ+YW[ g} Q7^n!ZIcb{$zoepB;wf4ޞEFp Esj}B}d yDyv *sT2)ݩ RU7&*w:-*e-^ P[6urmk-=( Sů'y$zALvxT扵/̭Yf_0AC?|0nmQ#]z`ڂNTjU؊a?ϼt_7׉:u??6.vE*HAS" ̢^Pw1A4bC#L5qcaC[\m"svx6X0m;Om i zX,4uŦ/<[`թF(hk`D]#ɽ5r{^ۨKD}Uv@vQCvAUxV#afnPH"?QwGqN (> \*xr~EYZ'*w_:[꩚¼g2L<=4$|}yҝt8vr)7]pn We UL LU6-v?YU7G"E:8)U}\ 2ĩ>@- ݸG7jimu}Kq!iyA(/6?J-/?̴shj08p*(S1ﴞp@r !"V @IT\Ʊ\ endstream endobj 478 0 obj << /Length 1490 /Filter /FlateDecode >> stream xY[sF~ׯohlnq: hlxd鉝8Ep F_GDbp{y1GjqA %v⺥R,pICki yfϷ!tWCR 'vECGZrA$ *cA!#IY))&kLkSNf0` )XFC"!:%QnO4|~Wbjvz枌8DrZcYOy@` ";LBPU+Y2$ݖm|O`Y8X".=XG`+5y}a\ᒺQa.` itTSz@ )>0a񺅐̗$c:)Rba^=W0c3&5NXVs"*GCV7-j Fc(%wNN)Sl+O+FjQn7nj3l,\VIאwͲGJDIh-b\PnQuǎII vR"|+vkk@m 3fHeQ窆Sj%:Rռ۪ذte\;ZO]FWP8v 0$sO¢#ȥ"췢%YC(e4ctY<_6++1 NXb\f#hGbPBC.Fo\4EJ]YCOB7x =;a>ʡIf+#A&`9^zcmҳ e ʗ1P,s3s{j5ZLCyCoЭHE5;(C#Hp<.z,\Ոk %}聯HbL a`S{ns!fiܟ7a?w/ح1ӒyvSi]KЙٴ::Ӡ joz___+WG~5v6.w G8$!ܯB[ǀsM~h=Q@R3T4=ZT3JvIN#:$u>G62 ^m}tv{>A_|&dfcՅt- -"_-P [(~ a8ybp ׾)($PXԱ Q^,K~0GڷӴ?!?՜"B\?a L)t6^)-iO`ruT6_v=d endstream endobj 482 0 obj << /Length 925 /Filter /FlateDecode >> stream xݗMo0| kځH;vo}*ӺUM- !H.MzϛAI;lS;D\r̉DјkN; ;͢1[;ܺpuWyQ BK4+D07=oMPA{y_=Rc VϿT`mv rrd9SSZYP/d26/ŕilbK;iAğv}N$x_YSib~N8BQ~kliSL(0Ls /Q W(hY\ZxI2i*Q8/׽r^WM F㛳7E6Ja5eu_*bRjx%!EP^_I*&/ [HlΕ$JiC^R#]JQZUu]hZ4f-,XZ80?/S^ S0I'Z CrQ9FymrYQ>7˾KygOވB|Xa-ّ,M|vWLp9,Ѥԟ~\6 ,>?Ŵ&ISu¶9e _\k1 \笭 }nkռ_KiCa.6ln9n`QV XwAvn|R mL!fmd 5 -f1D[ƶzuu$òrxPtܾx%yUsE]U݈E䋀 (QF %QDR~Jσgl EXO R P0K7i y endstream endobj 487 0 obj << /Length 214 /Filter /FlateDecode >> stream xڕMk0 :&4+;B㭅8GLB $Wz 8lʳti_gQZ ]ѾrApa4Y{CBnS{߲Ƴ&";*Cd=>-pe*ѩ`˞^x˿*e'"2 BIiؽ33UX.4QQO1vc4^\oR endstream endobj 491 0 obj << /Length 1901 /Filter /FlateDecode >> stream xn6=_!Ij7R 芶k1`[ M1ȶl %We[v IQ߄w،_/#qxŧ[(mk *3?..߲ȋP$f+JN$fKW\O>> G aʽ)e("zof=DS;sx<DŽxb]WrD+\FkmG0 r xx wN88elOw<1@БLX&@R0@FHM/ *PEUYlOLbthG}BiGo=A,nn^; RDgߏ !đi~OQU&pT:6K;x<+n$p/Kţ'!Ga$.CQZ)DÂfۂۘ=,\ρ0!4> `>~&DY < MqCi>]јT4]iѕʬW>E=HMXtf4lO,}];le\  ZU}KRUJdgWxNUubrt>BNSu  :~}`ηbIl :mG~턀2_vKN+o;E`2OeѱAʃcR)i umI|5v\0? }gHU~ǽ娧rԤ5ztTsrB?N")HoOX!Dbu1wWs%:!BwkY.T2*[4! 5({uF 5*-ʆD7tQ@z^@L3Z٠횯%LbThq&sԮoD!PTJqRmSrYAoYe@۲$\")KYpnܳ,"9QOӘ0D4b.lŸ 3@Z\?$goy1Q4~q1ާ9ϔe{-i;pӇk'{wMݩ" 8bYPD"RJBL2IlGw endstream endobj 499 0 obj << /Length 2665 /Filter /FlateDecode >> stream xZKs8Wps,Te3䰵&S)ZdV(RCR療$cjKAnCaO/ &d2^*$Qp ~O?_ΙҳuRE~}MfFs/BPȉU\{,]( ) ,0s*DSJ1XhEFIp7ۤG9'QěqN ᬾ5psUEU-Mٴ}MR~w@{ȴ=n R`NT'E`t^EccW U1AK"\K&f/Vz)fIj!aIqwiEi`o]~;mGTq7+/?vf) )%1,]z@`Hs`vhC )&,n ؎ CZt6md:އ۝wå"Gp`n`"mu>&P$ɢY[w)=v_~}-6KS`Nn2C.QHg׷~ďs˅A ~r3!GV zM釻ذ7yBGG$t>$L|AysܰIon֙b˞ѮR L~ mC$ 'K΃yMcOQ4{!6~8#!pϢlˆp+ypx C6Ic"E fsYՅbkL"8 }hr>!reϜ lTk !tabP0%.ͼ;t ,FXi"&aGt7q,sIhȫҫM]o= \F{ ~i$<lL`,.鬞vyC"eZE]XkY3/ASA6&D^r`!@BQ#iX#sqȷvPuԠ&^ᙒ8JBٞ6gxZW[ /YY4E×Aʽplq)CãHDžTJ,8 O&j?XPijxQOG}c#dsG5u8&ÿ%3`Gm/+=K]1|-5Cփ;-WA]$H>iZ:LvΛBL](z0r R6-Samo*=irWÍA%?_Peb' E¶B P V$ ٨lI\ ZAe [zBc9rHOYϵ'Q ak_vXq˕!#eY^65<(2]=vTP38 8da p_0ÿkE+K#:dŻYj*p?:QaÄG!'\JT$VH#Ʊ+w ;S. Lu.͖<4{ G^]xBIX^p:azFDt:;dGlvOBXo?A޻@ ɮm".̒C! Zngt&5iDq/܏ @Ox;N3Br=#p5׷ɶv]ٛ*lsڐ endstream endobj 511 0 obj << /Length 2274 /Filter /FlateDecode >> stream xZ[o~`>Hh89d EwSu &GTl=sH-ۻ6 ,r8s̹~G8X8۫Y(i.1p4A,am|}?C*U]˺˺R'Yɬ"xl 9RϿ⠀F,M3k *byxn*OxcTXv46,u(,W ݪlUg^sdaomY/5€ "f9BbD.JَFa1%5K|aNnf.9f_T7i%ݑUcy?x(j,\mJLCQ{(<'Pݵ]ͻ:!'$gmv\:_拽mFf;wyY2X`;/z{KvV9Dk&˿dKݶR`_eEaGZMK4SaP3eڜ#+KkgpkdɵtiƸa-Z} Akˮ'|ZY8|}1Іk٨ZS$ U0t]=(]y!B5z}|Z~<$VR)ke&mK8%ȝ|T 5yP_b0#K ?-݌m;ϛ!U:/sqGPB|("WyE0t$R0ژDBC D0o@r.NES aB!<BV9,#t8xec$`r#BHsBoJDNN{0y`gEnmskV/s\y>EG>u\#m5/.{\Ջrm#H{Rw4i vQ$\6fuVx ܮ]‰3r߹?;f h4*&!:;B^y*|qÎ#b Ȓ3 kr Dh4vC"O  ues%)s`ZZ2_6gSEsv_Iugs1&@v;v ݿ㳸1f~ Cލ4>O`p"n %':&;unarIq%p}YY{5ls~zގuTYm*X?ޤZ6n ؗЃc*jӜeȿ^ULVl$P~b?-ӈw6TdOٶOvGuyHUMBP <˄fG֋4>SxMb2+'RgwtUV/SיNuZ~}?4s2d+M;b4Wӝr(5o}rY2dyиPϰ𔍉j&}9/N#_~%HKkفNMz!Lz֐nE@$ߩfeA"I@V逐ati=Ǯ[z,DܚGV JUVҦ/ U|CWXb0mlm"Ldt W +'l5v^/T#sxapVU|*,@oY5`!^:* G|rqD~tìqAmM' endstream endobj 517 0 obj << /Length 2147 /Filter /FlateDecode >> stream xZ[6~_`G$E]IM-%W3CR$ˎoNc"ϝ!Xs˱^|C+$$7r={f4,I!+ 9o$zfh> 4ff?l|H|6jGI ͗(FۡlpAP\x* K3(M= [ʣ'k*``~B!Nby_WEuKn^B7}Elv=J5DAIu%|G*LwSQ M@=/t<:L,/4 skdGxdyX.{{8N-Ͱ6yX6zөVP\U !%?xU I$cBf&ƛ@0;xȤ,w ]gF:M.C4%{meV/o!㻄\FJmU>>n1J=  ]RRGRV7U4e%'`ߵw,Q$E:` ?tNHM/4>C>t.lҍztcBNBX+BNr}˧0[Z3@K%P2seT<:%|jr2n\ﶥq82&BfMfŝ$a~tDsЩw-y%4!˃a0`7hؗCc&É9 lrQbpvm=FtDaOrχFrdl/ I]]xykxwi޹A)'tvҤ.+CMשS{$[`@6&$\ݦޤl{SP=a& qHҢfP~ LԥLĝƒ~2R 2S wtw E{ӱu㘛x}5a+)ZX7jKau(ikU +Ĝ'<bQUOjpYf4#3E]c}-4WXJCW bC.ӪfB'="F&@fVR' dyDgQ1VlhFϐcb jv iSophV;1QYoABҔ]rclYQfm[\!O:\B(bW+]vU~H j NGN ʝ7 `-D(!Qt| b}9 ]xB{ƶ'ˆWQĚ3uT~F}tڢؕlF_n<(8 #C샰+4@=Ei~N8]anβ|BF0 5{LB{z,?f)ݕ9S8kev'Xp 9N_:L$S> /y>|=}XЋx{nɡt ǐLscRz`60[O=83lc;mY% ݻ.uux.Z`imeN`?> endstream endobj 523 0 obj << /Length 2073 /Filter /FlateDecode >> stream xZ[o6~ϯ#FSE@^(0-Eڱ$oC%YӺh1@Lss>~bEC\}qwu5łnPNx c]񋯮C"bYN_۹zJ/_\}uw+ D$ Xջ_ 7Ahu8FD QC`ov#F3(AɀP0Al+U>΢B ,*( 10 )&Eo#;x(-fK;f񽶟J/& ?,<=q7Ks(qbC7zL#K;bYy~HXթ 8Son!d}JooE'Hy1@Э'`WuVo XȖ8`Võ AMϮKc$D>&u8it G2>Z[@*Hҋdy^'yZ]TZ? RksN3 H^":z FDۛ{ϗ0Kt^-@@nu_ð/86uWa+[@ѓ"#6Xّaz el[EgΖ$E&!vu0ݥ1b{!PkmGNb$B یtuV4=3^刀@"ekJW~q l7{bl dvԢx'3Գ$i&md氮e!}Tv BOc2Pt&PH-Jαuτ!uYVI43md)GRݻgy:=eܐFKSCm?ngXϩ@bMV[L^0 L&uNVkg~kgZߛɞ\LDZ,\ W<Vc=zc藕=W^7gt- nÍ5}x k$n4j>7R AJ=؀u>a`%X>i(sdv) CN9{UuכG-7eKjpB ;2q$|sV]bNQeʼ}I{I&߷m40H^@<`#bc@[v`ml mWk<[5Xwu ;ZxSK'm<zqPnzRfeQM=b@TkΙ@a8_XN(yea{wb[P=yf8wkD*)ݩGsE:,x=r+v31%4š(Jap(^ϝE &R|cKlo)Y /zJݲEONh ~2Ma~?92 6ekOOL3'y8;EKЈE$0叒>I ,˩bͩ d#o`?ciW-YWØϩ}A'^0D[/z0Ot7b3VNbk32~}\cYƊaKF,# OEұ~TR#{ s 6Vq /dgmwO/!gP^O#),!=25$@)8HWxALyі;"\M#)䋕nf6\fi, B?tr Nu^5.i>|o1&˧_A-U0q%X1Vɽl滦ttXf2D?{GL)QGHqS72|+/-WNQ$> stream xZێ8}Why!)LL`_&Ӌ}H Yږ%$wx,˒6MIŪbթSqn7_zIAw2D~{wsӿ>: #7+UQk;Y**BM4nGD x5'0ȗlZyDc; q[S[s4~&C!b" uT/AON>9 [yUvmV3UMCޔHUJfZ(l-c*- 4Jb~҃ڄ>a0Ͱ$EBYzj({t*XWӄqT$,uʖ\#CF┐MF''hk-`Hg3)] M([]t^Ďvo;> L˔cAgb5o1IbuuD^$$RgOT5F` C2ׄנ!&'E):蘲wʵmOl o7.3 kʋf2,ohfYEE5[釛p"A#uaZk !_(P?}"IF՛eM*U4(O,Ǟv*V,”MZ9DW3 J+ؕ=\'H,uYgY$D91l= 5=y{D %1ZٓJ`Ra J>Cj%z_[%-lki4px9xf^J]6Nkz}j{?@$!oP\hJYT+J(HB<&9ćatdj:(#>r֤F>F&uGżqiLV[$:#RAdtFdjfen*UCƟC "KTTVũ"ch |ҴMyNHªƈ|~|arg?`zgvƈπz `V2R}@J(+#{FsV YY~ 202O 8z!e}htzebkЉ [ Jq=$—ՙ!ZrXVk H`dWPaNDtE<B?ClOe>84vKLCn8(0#TWÈ)k)aXJ -vle'u#AjyA 6u8u14Uxo> v!4^Wӏ\K4ԗ[6Dt[.>wBDM!)ϫX((d!]E. k9C;o](#5LUM8Og?xzA(m9|\68: endstream endobj 440 0 obj << /Type /ObjStm /N 100 /First 885 /Length 1628 /Filter /FlateDecode >> stream xڽYQ5 ~_G!۱HJtR8 U[tJɼiɩjڀ멍xOR`1(06* -T Zi !{R` A/(ܣ% Z5Bh*"j=U/ Ĩ!c%q Ύ;V$sa9BUVBԠC7l!x ޓԆb]BPFF$h[x뫊:&t `B3 ȑ}pC=,n}EH-PB9Rj7}C} V(i)ZBरJ RBT;xkZR/`zCAW{7$s`x<)pWBM6qǜV ?3`@װ0m\A~@[Fa8˗馎;yGJxv8tukjբ7<ۼ~O7u^O"E`󝱈#ތ((E ^~l8Sy5|=|3|qCc#ƢTr&DɅbsIkS騕ijrOޢmLi"LfCi{j!1e.,8uXafp p.6"PEؕEemoE}1!rVB>QT+|Nv~3!H7eC#>Jl׉O2~|U93>/OK>rb1,h";rV#FñƦrǃԧvdgyV4']4RA6wSsg(rlZ/<jF aڑ~vxN$Qvq>㡤G#TgBfuY4q\Q/amkMn>=6-L Gjwcji8QAK[*dV8=Ϛi|fGu#| q;A0*-Ex,˳5a p3uVϱVbKdTC Zjf#~d)# Zn3`Bx[Jh+5ݥrG75J2Gs_Vteq䃦 IEA)./,A JF(@ BI.zRUdk'O*qVL~4hQxjit$UwEY.>x=+R*.$rl }7=9H<57'u_|ܭTX)m|Jwءό( 1%Us ̓p/sG|y[fp9ܱ#8r>3;Xu †Mh=3?n(| endstream endobj 542 0 obj << /Length 2911 /Filter /FlateDecode >> stream xk۸ u^`!R/9Cm?%@i[,9C/>XQcr837EM@^rwW I"w@HAJb"bܭO͜xryI,n-uZk qշ+ЀƐDէ?h BW߮#ɕG.cAFʒsiHޢQ ?MZmteUd>f"f5(#i~k_*$S9EH KoIΑ#w 2"K2'ΖRUq,Zr[ֺp!\K`M#T$nYpɒ9vH[?Q:&eQ:>v9 W0tP^P pgxv=6NRM66yV|z((a{gu &&.#z1Dqr ؅؅ѬƠݱII(mЫ_&/X1#RVNzR00€bG7?ou lA&28}Z"N e[AX%9'Dv%+=eQĨb{?Tvi:{$%ƘqTR=f >DaH("u]9nI}Ը6 ^澥*:_!MWYa+Ѣq53Y`5&H};詎UULlڛ"^&ܙND1Qa Klj *-H ~HYB$SXVپJC5" 3s臏n.O|rH.&sA* Y{Vjϓ"ȇ&o ZHA%ܘksktT>cަ %N7 i\͡OؕW]覜ns724>/2%Uƒ9e xh< endstream endobj 549 0 obj << /Length 2335 /Filter /FlateDecode >> stream xZ[s6~ׯ`$g,Ȥ%n&SPlsK*I9x%;X;s(߻|7דs)$2/T޻_yu9ca4._lV:*ɳ+;F:.]~~=yu=mBߣb@ j-a"ZyƩvw]NEᔲ ce=cҰPM G=pe8-7J Jfg4[Slz6/dCA_R1ELYu=WMQn-' IY&ᢎp "yP d(^Ju#C")\ތR`y,} G{}8`JcOhl|.ڞ=Q$t;rpB]b7Y!8#d]ð*YaN(nh,@_"jj$ނ3:(z`e^-XCgP=a}7ěmV:rgLy+(w52o*np-R3O 8}1 -H$h L͑6]_}2b~*Xb\~ՆZ Sv/ZeYLzV!K B^ v&n@L0~#( #ldtp:8JfF (@%jBXf޸ ʠu=cvH n-НyB=  πGDK8aôIuOvӪb.iٯN΅rq`)Xo6x% Lx386:r@{ϻg,?ꐪNTjOudKA &v Ld*I <5.Fk9>Bv83L5,A/K~t NY^ !x JLz!X6m]Yq~gTMJ{I"w!_4wAJ: _:3~m74ZO!j noW7?}}1DHխ!r ZQoevlp:}47aDְiENj׬QsJ/f:ZZtwtZ"jU-Eg _bj= \>2FŁq\,cnDf;ih?wug8n#~f@mjؽUB;opN1.iՙw4+6TEZI*'z[;T",v]@rX2OxMIo RpFs96/Y˔} 'v7癡p ā[?Ff7#v|1ou2vk7N7kn!/>[6e/]Kb+]Q}/7UWB%<mZ4<`]l+j-`D!3x*]VU[CI  NƠЉz& /"+3ߙ4Uf_3`DHrw]-sh  =FAB#Bɑ&cM sNKMqWA9@3џP&Ad6.bxm0Jr9MH󲄘?x+K endstream endobj 555 0 obj << /Length 2556 /Filter /FlateDecode >> stream x[[oF~`dd6i,v-Q6QTI*RDEp89s̹}gdyWY(TzK (|rF}u}6߬ 8Kl("/߮?>2#O"G"o[@#(Zy\D} t3{~=1ӬNG\JOJ,, `$V^yKXuRG0-qa|t"S bɵ Xp"~pQ^D_$tz`XVbćنt tq0Ix;mWb LB@}MVUFE Z $WI%XEPzQ%L_Ӓ᧵̲cϊHDJ"Lx^]d=۳<=RDv؉ٯ?ْ@cmd*`:ZIũ}{,c @X 'w):b6yo(=^E;3Iړ*>8 aF)Hz1JxH$,L4ųoF>qv!P_iCg dXrx QR ",MDs"!5=H%x &}s~ z>Z3UPYŷyvS)mY5,7 &lS7m1M"7?LyѝG&qa"18HCQ_!e(b! (pXցJ{b qr0:?bqh)>›4~UW\>Oa88O_mϐh{x֊'Dc> ~l<96'6f1Aj0h l1 93H_Z䯔mMm&qi_bCX RElare_$ۨ6cP!yexDAu9RFs}H;؝I]RqPD; ьm1,w1[g)RN&PSKblu j헃S4A}RiP/idqkbT z8ܝ֩Nu:b旙s]E(7"as-n#+(CAհqZg!Q鐛uKN!b2"!2%d] Qnq1Pf I٘.:ۑ&8팗5yYMt٦ V2Wsr|Z%@K'kU;W  E*u-8n(< .uW@*jppjd$ .LAٺF !._w,NNS)|dn=j"4[{nA:; ݕ۷y;Sh8\=31=#Q<dnn^Ǧ]{,$Mλ3*IJ&x&+>.fo\mͩB]g_R/dЍȭZjLn7f&^T[ X=&7a\\X{-Y>qWѬ]O,uhn,@M:]3'@V} h1`M|feDc J'/uؾуw"#+mN+hOEбܭ;.}AU::AdP,;Mcj|vw׊•She֭,_YG:KC61f8 ZAj P,M׶~ϫElH<_Jjvvf1CeX.v@:/m햾~pY.5?> stream xZKo6W94-HLxO` wmaՒWK+, X%]_D7Qz/4rg$'FWs??.⟛muWteSce% %AwՇWg=ðBaK1E,p$(A4у #3Og:KɔS&b Xqc[v'_2:$P xa>~h|ٕgX*qse/*-}"}-Wrg}r[7mwnq ۮivF_UYTkv朰:ja1A8#hcBiʛ$OSFQXϔm~7v\0d|nd0H@L'LerՌ})hKۗ Pau&c(O~IfԴ tn>/*0' c1f}/ ݠۦ^",wEJ:54Me!*޵R)às8òLhg3Ggޞ ޾fek1kdDO*' u;2Av]s4E&\uUec&$.kU@E׵9I⾓$Gx6$lIf8My%l\r> -6q &.$.5+He-݄xs_J?̛ƭX@޽0ƴUcƌ[D7g3pŵ6s>mB)+}nnAB7enrq3#!$z+Z{9Jy_w;qS[畐"+ .J1؛őYÑSCI@4z<єGdh])V!Y0zT7dp,I(n pٞ3`h@âGG)E p=0L!]0Š ΀,x69´th ,z%O+d ɐ;~|qry1fb$Еo- Ɩ°Oa'sjn\zg/,G7M./ls]i}l'qYne 0 =`;g`9$i51/4A$x%1Ȇźcu'hI}ww8 --vtvUIjhd>[V3H+y g7b@Hkbt?m+7{ $zvѵ%;|^K(Jӹ ;Efq+53HdIŭ3H&ްPV9zNҠq5XLݙO]vSҤg^7Ȍg.9IN7 pN d[/qZ@1JV~9d>'3-ЇA[y~@>J7F$OͪpxEs̾縷dq(yt7MGo&8Ps{ h ůB{4kQ)[MkpA_OGZ4M/#( +#xʟ>y|GOF| tXxf~ M99YX\iq,*<1 w#>ՋCb{šT ReO*ڃ+K1S{xSa%hFtw\: M_/s(VTTkH@hO{$ٛỢ0UmoQ y(r/}n9`%CNN<0%38T-'C8YC/(8[xoMlҭO]T 8yVuK"H+i5…DDK4 lj6Oc ʡ\JN9'׃MgbghI;Xb<21F-:XsgȏתQA`?ߊ endstream endobj 575 0 obj << /Length 2860 /Filter /FlateDecode >> stream xZYo8~ ،[;d6yv`Kl6jGbVHhvb_:wtū~,ȹ8~3 8MMzo.8YXgNV芺cw77_pu{b4v/Ni1ĈK J6N>0]TNThr(H}kwO~ݳ{YWlps, l%PZM!ֲl+\ewkI"dʜzn[Tet#\5ˎ:FԛCk sJpJF{!? ,cݫs;yEvEv[ؗ,-$c 80ߪe{+-,yM^|l3D( *Vj$ġ2̊״h)UL XSD^|X*mxMlnP/w>D|ûO|# Գ1FZQQZVv'όE33wB,?USws ?NW+|+DEϼhd́~>0,lz`A @olBc仢mWčo C$I$9”\oJ|E{7s'bC}!bhFs|k yĸow| yr$bnk$gΤWi6OuFupL)XXs7Go1Ok%B,!F!0 :Y#h7dn5^" Bc%2p;ޭګ! 3k^ucROvqޢ[ +4/.WLy HL*{kg3G{v槷Hf 3!Ht$RW+pIKdyuTv!KI&eR`uXà ,H" >g qLjTx@XBI8ORً ]gr+`S>=[P|m@#wܨzr_vxSKvRrâ)vŮ{7,fD I4Ufb'r3>tB =Źr`Cۣ@jV;M(6CC.9s.u%o7E)G\^R咰cUF=S,ߚRP; ^NA-ջu !zE"8mnunֆ,ڠۊl* vhbBPY#ZÆ% j?RȰ7e'6#ҰIdAn.@sux\ǟ$W\a7ߒ8 ( <4 =*ڭ̯ tx}EQh=XEQn~ }fv)- osb).Y"a3Cѩ33D]wt j$SW41NIBYm(+fѭov)Œa(2=Q @Χ]5rTF`}W@N*eޘ Ȳ Ms~P%$ &H/B T5=--1 8UQ2'bS`l͌Vj*P"U:@sKv`dvrKO ˌǚcr,J3t"P " !4릻_:(uj8CVE@{.-|YTT!6uI `nlyhd#^*ݞbөŜ?C*uklg^0qpoFߤ}Fe_?1RKڛUqi({(5Eco5$<^̐tg-,|92yv?C<&_`O ,^MKbLj6y}DŽϿ E= :,@$mt< endstream endobj 583 0 obj << /Length 2770 /Filter /FlateDecode >> stream x[moۺ_l Ԍ*-u-aks?EtU/< (y Jbc\΂j|  B“8jz-!)a4QӶ]S  DDVY&:$+iC>1u>uS0D#I8cw"0 Fq]6XbZm]2҅tgѥed< h!:6nB]ߍ>HxÖa/n)cx[o [hUսctYE5D#K2:U v5Ðt8PDI#! M##R Igl"k^Ġ>:~~ozW@r0@tYY9r}r;۷?OS~_.ҁE(N)<~8>Hq dj0A` A[#O )W;Q2M%"La} Cej˵3/!٦練]*8"\ujWoo/.:lh(KXv1hum#Xf,D3J;elQI[r.+ˬߴ\f~զ5/5٭}6ةv 6Eop٨DYU_ lVL”`:m#!H7(\ `'K}1X> qY!]bcC+X~6ꁂE0Dž4mК> W0~2ƃ/C LLf .="N R&8[ !q>'"^"IuI"WT$y/^ޔf(c+D:/oJ$%4 m3z;?ج%Dܝ:#RtZxgepd 8 7s҆t6O|[~ݳG)jl.pMQ'B ‰`;B$J.6P"w gb]qUAT&$QՏPu_EOtAhJ)cB?9ɨ&#i .׃NO~x:oiP?wy_ sH%G,sQy<"R} 6ta 4 ~; 8* 1۹Й*ϝe`Լ@H 'p1Lt>E;K7 bSIA:F2Q8yi_CLA8,\2tb\8kP j}@a]IDgF> stream xڥZI}V Z8 dA@QcTH=yrY,zva_~ A #,x^PҀ'E" Wٗi \Ve,۴ͫѬ}Linߟ<|z~cфa/ADz6)F3.>!ly+br;=PEi(Ij=bPp2XÑdSOl 8yJ2`D(PP{HT1(`s"kpbL^(9@ap l ځ{ƢK"ܴB#ʴTi#X7H^Qg4-t̼(LfYZ |"oZP͊RVpkg,a={HCkPشk^j)6]e`aT{5>fUߖOSn۸k*W``# ڕzGP=uZ6o(s!PF!:-&# )I9E!:/v'O|pBNРE*OiYk+kIcHjv2˿!= *ʶ!F AFP]ec0iKFf aowc_Ci;tt6j_;[B!q$yUt]@gh*!b=%a8d >8)!~*y=g}r⧋ +M]U<|^ ;(uT1*>;a^&2nEHҗ?O 0H4/EkL)1'{DܰTjei(CB ~  `Fc$hFpr5:::`ndsKc40X@11h{ e A~aTD,0")ن+_+G&UyEv*B7KƯY GVdC+nMO5P|T#l%!lDՠt͘p';Wg&6 }y\r2{VW>Tf~pA';S1^@DHi]sp&u9*D,V m+sEgPAݻ(WeUoun)L$ASnq]7yʪ_jt-DnS5"PB4fKgΉ ֻ)qR3>ǥql8#gFe  eʊ($W}f] ۰ 8FӘ,y#'CNtk|@~IJ+Bׅq_DPjv16OVB#\x`.|Ȼ#X$JؑK/jTm=]#STHޟ6[b*a!Cڮ(흇y@evS'Yу|؈^tJ4/37 zo*kF&LJX@7]r أ>0L8k ηGwᙟߛuOqb1Q=׋;[o{s0oundcIKi F}\un63Z[Fc϶ZA=kpkMB_)oRKfSMUKG[R0@pjЦ1jAy[Uj5B~b?Pە NpWp=asQ|Ls8NUn7XDH'"`W⒥9oSߞPCVk]-g:Gg>9҅k_u4Dz/5=47A͖-j~ @fOQ GE a2VH]楃bl DGt2$ "m)t  7[4;Yk3U e dXtko.|3M]]jMKyz5I܇!.Fɑ[ CP[&(J}E,?lm2kn}h{)DUH ? ?: endstream endobj 602 0 obj << /Length 212 /Filter /FlateDecode >> stream xڕ=o0w |Jḧ́"p RBZq*uA Vld)8t ( ۬Ll OyP !/-6D٪n}}M endstream endobj 682 0 obj << /Length 1324 /Filter /FlateDecode >> stream xZr6}WQ`Wil7eq3e&(_d~1ǜbVgp|w9;9á CGx&!Ļߋ/'gZȻaˠո1'gz%CEBo)P!~<*b,yeFO@0 ȿ$`^% ``DX>51_oDQ_Te1e "ّE @eGEx4Zm'w$B6a z\.S(𐊬uǶmY5UTeakĐEx+P eehp§8. GKEZƢ(YSG)I+Kp&J<6QR:,vNd}ԉv|ݨO@@ iUmURNumB&}o-*i\H`K83n۶`Dlps P^H VK޹+Qj{G(mtUڴ ?]+׿u'TѦhYeU:GZ[N\^>}q$>CRP1\2@ :rSƅJʆL4Wt֗X.N'.F~Q"O*Y _. MtHIp# .Ekyy":܈kYZ!t1S?墮4P(KȈ.m-iJ킝tq >F+DX7bÃTUK{(޽xVL;qӡ\L&]&a̤6_Fz먊r!+ UU6k볃Su0ĕ8SlN"FĦ[&;=@9vϓqs;/lQ+(=MB xp\* ;*n ?#* ;y$-\VMxbrkGnC-KCMUdlV޷['+;Lio^*b~4/./g<5B gaWX聵lﳶC8"N糫/KLx?:Peʼϳ_{!}{_4p endstream endobj 545 0 obj << /Type /ObjStm /N 100 /First 894 /Length 2488 /Filter /FlateDecode >> stream xZKϯ1ɁzHۂ `H:8tpE 5|ũ}{ AtŪdUJ%UDqm[ŵ'%NUGѓ K0^KjczwUFß Ւ(U&ɆDiNTR1`pg ܘ?D(U+ &1J*n* +6Uc_[X ԅR( ᆒa& *ؕ:@KՊ4$iW):c]].݊KZP|:%y`:aB tp Aa0xgp#5Q .mn?d3D0׻g_>}8Q3{])~J@ ߞix2p~78w7_u>v>|i~ehq?w$z&sܩ\d%K16xogoegxv \lˆ XLpoPivE2 b ʎ1 PɐdmE"z]u€`-X_~y8袹;8*Rd V 3|A0W\+.| OGwx[8vƽ 0j/^se|uçoN tWiȾcc㮍8ؖږslղ!WnZF~ JCpbF2cQa j,!&خc 0|6<`7ב 6fOר\iEgՇ8b%Z n t=X4۔A sH-{"x2\<}$s )e01t}2Lf3ڮ &bdzgI$tH*XdBYBCM )SO\+#>ks~X-kq-qnjN (`T/$jHߺBHP{}DF"za|>mBf2@p\?SPŖP2򒕨5' .sޮ=0z^whF22xBiMwV:=ӭ5`[C5{FcJ]m%Y~J&4 c)(I<d+beBJvD=/U/;60#_Vֱi"oȫI >׏!]1g!a6<)gQ{}JRd]˖ |"f7)'B2*Z wCqc,w%%{wq0Fԡ0Oa)IZUJC-6VfcZ~D7aNOpWwwEyη%8\--=+ŕ*q z(QУGA=z8qA= z$IГ'A/MwE4Es4E _A/A/ZcA_d5EjЫA/\>͂= z,YгgA/J7͢d(,"_ZkAmۯ 7W4++Oa;l ?0@J;$`e4=ؼ9qO<0Q6d·JbLȖT8bB|+q RVøDi>|c&d='1߰䩁7>}5O$ e-gןtdJF VK. {4at?5т:t~ )`@ M9\vQU0![ |\B|$3e^-0ѶT@W<7v:_E++mzt$&"*Uߋ.Y:K[jSCf;Rk+E.K^{KA@'|آP 0Khuƃ:q@$dQuEuAܢZB[bޱ/V@"֖]Q7CKi^Q=r. )aâNC Y%Wym@+n?7A(Zsm!~*ѷ q׉*` hWU endstream endobj 717 0 obj << /Length 733 /Filter /FlateDecode >> stream xWmo0ίDZ\`_ժڴUX v@ Hyq|=w; Μst@2ߑǐc_Lg'7QR2,^P J`^s2w~8,D.%(sD0|XV3O ]:_'u1w%S/w6NxA3x 4b-*L~NY?v| f\xw} jAzsdm({E:ko HE;\AqBpP,edabM:>ہ-Um4RC%]0n>=€_zC#peJ6Qfr:J눦G8ԗ==6`Gu? {g.RmU9!bО &4 6-ڭyf3ơ?{yg.L -kUz1dXg A$IO]?횷֑i8G)?0 eozH\3qȡ˃c*7lɮݹ[mbu4c)jם9 ,2ƷNRS kn2\'a&=zbv)͗ϲaT*gË#[[p1ﺔ ʡeglX'TL~Y4on endstream endobj 731 0 obj << /Length1 1396 /Length2 5927 /Length3 0 /Length 6885 /Filter /FlateDecode >> stream xڍTTk;UH 20 14ҝ4ҒHJt 1߻ֽk{?{{y>V&}#^0 !x% ( C.?nVS'o%)(rD%$@ (/ C :|M8 Iw::!Pa Ppx@A0qEhr?JpH;!n>>>| WO>,'pB+dowu0GSC"x ' G僼AP s@UB gJf8=~YXʂ8.0?@e9 'K? Q>B>)C?\M5}>Ju[@{]Ew䋥 ~缌ێvӷitӤhf =hqd=Fu]i>lT4#PS\Nt!ŮxC5{פxLlf-w>d+} qO4C7G"U锅qM HJةݕT,sf tOOܻ_4Qwk>OHNb5][+0^646~%,ExEǤWy+%ĦP=A$geBEՅgc3#c' ;[<+&\dȤ & >-E~zAqE*pLJ)Po5+`q)GqBZ\t4JW,l vxS \ᕄ!fpFdؘ,#- O+j Y=]-] aS$䋉Ѳ\OCҡSKuz/@5-)y[TXld#l=TFj0NOR E[P>};Px@-tERֻV5&o`t+戸д)4+*푬H;l9,ixbNb$ 'љImak'ss%RgkH?ʫbYq"=o)k\pj,*Оd#~%䇥MJi͎S=|gDecfFQ>΄1O sW0xO+vۭhsk|%;gTȵQ8jѷєR~7+!cs10''1''VMi뼫 W/bh>PqMKG06WPVƴӕ" /ݥZ,}b>#R~-S2c7زvagL!o.vl‰4ЯwmQD^1b4-uK_4pڝO3ݱr)5a DYJvS6EPIt?BsEUbP8uή6yx:o𢻤avd˪d!s[rR5_Qql6{&>A3o|y)rl!lVU>xaY;/Q %Ξ<;, {ȓ4r@ y~Y}r=¾Һ9ƣ3|6-Mxk-u -ŏ.1քQ7 hOZKu5esBF1" kQ6-TϻzWDi85L2؉Q,&1jxhgk@f/p͂&=rRdpBXw F“ ],N)gw3ؑvt ߆<IJov[dc{o\F14z6<ݴm|bT|{.P]< &,ӻEߠn+9{fmx!j2zKnyԨ.'7Oq7,nF4D=4r(%KՃ T7;b@ח3YZoq)v/); E7Ibz u\.jEJbhv 0yLx-p8=}/֙*QNഭrN)0n2wOV6 Sh.\w<}RF,6w6^;C $I z,- ;/3M4OD{'j@߆3J"n= 샮d= o |O-EW2LVC)ձŵbؑkl 'bB/ykk- 1&iQd#,ܓZ&uXjygFs;MT<b^vkNɮ>ݔy N?lӞep3V k{AFJu0MXRn2i}rEGnE$9ƥEHa{dwf$aAsӕgv*jǑ7Heh64з7Fg #IaL~Ow4N?@pU? vv9?\f S`y4,Qz" \Mfh~mZC,(7HϮ| N ʊR(T ?C/-b]̡CT. sV2sMeWݳxSդ)^mo 3CU{,Ij=tѮgtP~H-uBc79/g'L;sol(B>2`xI|:#L30NYiueEyf[}1wX9<49XŨZp49sתsZ>ǿǦH ~ޖtf܎O},vD2lqg!֫s3(RqǔC:^,wKCpf 6Fr_ZW1ff&g(HogxUp;/ L_.̑7ehsRy 2C#܎Xtfǖ)k'.#ñFoeߦpQ&Ei`uXfi!$}M/ўV~{v8_~PfZ꩸(tzU^lڶG<_dflPC6\c5qPv5kF>M ³ۮHoœF}AZL$Cc ͱ6J/v)S"H/ ޻3\h\|ÀtE:FB,.'4f빖 LIR5T ~aswKVREM':?#ach'>sI< ދcplB$EoѪ$ӠJhKOJkppn>LI^}dת-*y[zgw>XUI%|L@!`KX4)"@<)\۰]rrM1_.Ml]Ww1LtGg~̍(]|W-lPᙴ4 <;n_܏~\vTo0Kf@TlU(˼l+ S"|CC~'/`D<*O3ilm[,Qu {2 .rJ_xK3䷖޽s@Zx!|lSznI4;Osܡ5'ו56 Qֿ4zvjxޓ)M;2\/Ob#"M˯Ͳ# 52SXZN\гbkz!N.7:alˬZ~[,]A8cÇ|_̴>O?VBƉ6wT&g`&(5ŀ/TI B747 tila%gfg9kSo;cvdeH˩gdžr\6)DHP,ZM;ք= pd 2G@Q70QǦ5a۳"[<ʮCzutpqWZ^hWBSkKJ`><4tާbpc։|¼Bqf i5 D"*DHbgC#["/Xj~J ('RT}*GWٵB3|Ku]L^3sjTOhJ .ui~*{Hf$n:^~HDTy"ӧE/)m$\mX<$JT<.XF K7Z o ^^Vҳe_f^4FG 0E}2삗 ɍٯy1vDnx#ŘOh\IONy׶) \.;0ico.ܴl׭m[>12'fZ;dԟ-.{9rsTrV => K|a؁I3g=ϛww X>*^6P^9_ň _ό!(alAnqy&MZS0o}Va dPSR, R.1k<+CgbUH tPn` yڊL+2Kfqj `{"xQ[x|ߧ=ق1=@䢆H4$!Ar͟ҁI*޷8 4qB& T-؇PHDnZ92|(14PD)G:fAB BN=٠ph}yiLE]bWlr ڭg endstream endobj 733 0 obj << /Length1 1376 /Length2 5980 /Length3 0 /Length 6926 /Filter /FlateDecode >> stream xڍvTT6Hw$CAf!fE%DBZAJ:T@i~Z߷f3sЁ 4 # HPc1C ¤H⯙D@ `mJ0 FN $.-$! Կh7i@ 扄`@Br+]|ܐv6xm!)) p@ᆴM@6H)xe1iAA///0v{@pD_ Z0gğ܀=xAܱ(8 n@.?l!п A;P>H`tB*o ࿀0'w46 C:ߕy]mo{6nH;E_i+(;n# E඿{>@!]=jJ!Xlv ^Wz}o/3? `mE`HaߎH8X#(dǚ!S{BwXz('WPHCzO)(?a!@@JT$fс!VO H){J*yjg.-4?7AloW 'n sF:`9_U꿡?DU:Ga, $ #Ucc1~) Bݑ-(|Xy8bXZv!(4̄̇e'#Ȁ `Cl-ڍXEDAlϰ>ps7 k[7†4vC]xI<0r{Tqog&>Q긫4ǦYxue,(( <{sqfHZFeeoz3p"B[djv (:He\cS͑pLht L6̵|{=Vo)l}`!͓ӡpuWg*Cz]Vڟ*wLz# eqsNH؍<zj)ۦMu&i6$+Kf@gLu- kSwg+︬-$=7lHL*ϗ:.y:#WzY-y.tH*`k/Z)4tijJ~Jffۦ>*'6ڦ>CI@o? UW,+ 4||$e\0="p']u#+~ѐj} ;aLc}0R<Jo{}E@'$ ۙqө^JCj`=߾bJ|a}IUoɡG_#Cp\F*ٮQݞC2_jM]nf&1zF} f0Q<R?*VMr _XnXT%E?&CnԚܫV ݯJ,kz;\+3ҁ&q䄹cG˩cS]3o|G&*vءɢ-! (8zD.R/Po%KsT>? fޱnVڏF^ȅ[ѯ GYI߯'QQp2< A˦OEl|sbe =ҡNLmI`еqIHPT3<'=sթ\1{տ [5|g"Y@š:{l[ؿƥ!"}İp|[RĪN%LI6DxBh-*2bQӍ~i*xepL(fE/PoxH|6e{!{"z*=;!vC1PZUOז޾Mjr\3cym/!%Kq ZxPHP=x.Q 5y$l\56:6t_hY9nϥ"8rjl>Seh||q2f Yr?T?ϊO*P,7j >f4S i!>嶙=hTQZBH J0*s&awfyhy h~OA-Hw djx~v9\v7]?[njIΊj̑, юYLMD.kǦIMp3fU)浏`%2U4(8Et70Po}wJiWr bQ%?DP-*>R&W)]8gBF,e#XL<|` i>ٚ~%ܡ"ȋ,;>,Yld Kh-T(3aZ6̣6CoJw/ZhHhhs+Kmi۰CF&ԾywʅTwSv$hֈ"bQ.Ǔònae!{y>tRl̮Pz}6iHtI1 $.ˇ鉭Sֹ&⹈{Ծ_B4dĹoW;_-ɜYT 'vQE<)48H?&yJފM\GM zt? <]wѰ?#s[i?be6}27]NC\W*JS>偺j|~zNC֪b&2ʌ BsQ tϛn &D=TK'u]ƹv)% 60uL~}s^Ib>y"\ݶ~4fy y,H=+H#I(yj~|J_}8 k=mξȮ}jc'G2KE[-6ĶWvm~VxiX2k{}4ũZՠ]v²6 tkR8a:w'y#UOpƲ9?+H/AΪ Xy)=σ/7F]CI=49PӴǵ^|ǥ* [n-%=tY*c3 ~%FhNOTغMjRzWHs&3&جo4_Ofv!p­ "WB>_E-sYMsRqTAm^1nn :ѩH6,ґs-ƇNJJ{hAEroŶ:(Q&k/J|9m3wY԰)KO rNde6Dl:gGh4RZ+.( +.\'PQ=kOc_Xk(ޙ{`B&;k&}^rCb9`ϲ9@~+Wd7/E![u*&/DWE }Ï T"{3657Z @k+\r^H &44 Mx?P OԴ6Dt{a޴SĉsjԮ5JJdu-eVk殟wϑMw#V$+![;M6H.tϐ@&'d^ajʣڶΟDɌ{ZR. /wB|@bO4~hJgoi&oHۜ0Q}'~6jQv4rxWav+Zs=92/Ѕ8zW~չ 2vqZIż7^wqH~FJ&%g96^[,]Ea|ڦTj`Qxo5p,sn(:Tl9.'üE#eI% #ٲY( `EF`ًU.$~.Xحs ٴZ20TiCqfn~ZV)#=HܸG>\&y`.Koeƕ+$c|5Jo'0M\.gLRK1t1"GcVKZeMi2g,0ZcӺg__{*_npRhi=kFy H.V2 DOWAFȵ+]53Lu Q{f0fGH^{~+z9cK B> stream xڭweT]ے5www ww'kpww'Cpq}cUUk֬TQAhh"102Z:9mDƀ#h+n2hM VS֠z>v:Z(?^Mv6&F`jamSP&U@-U ibk`` Pt20Y:L,F@[cJsdqL,>ؙ8X8:~,f [#k'|Ms~D|> G# ;#?x @vp@#JX:@&r- >r9XM &f&0uu[vvn_,@&֦,9@,li[S vc'98 f惄1 `lbH ߩ E7qUv_%| ?. cX k!_5L|AC fF-%,\M-@FSmW56q56XŧjnadeW925W͛雜7MMRPfA?/ QQ+ fpsxaZ` (J_``/Çs!,/-SSAոكZ,Avuy.-2 I޷=nk$\B4=ym\tLȩ5"=.6s2o+)M9]<~v"C6JnGè?MppO?248uݳG@Og4p[g儲K 4_FF~9n740I픁;/##kZ*&y$Z6qg3R!Wn|l QO&Ǡ ZFyMVnT6Mu9) K=畭M p ۭ|>@{69L|ёeRP3Q,޺Y!p,#s7;m6ө;^?X]y0792Ť2jR:᪌z_cꓨ¯tb"FW\z'QLZR< أŪK*yxZSP}9NM/^JrÛ؈E¢%mJUnM8^oʨ%NJB=/Rn,_ݒV -Pnm)M ֬$go?5W/quġ`jfmJklP"a䡥r * ٿsKVw&y{d0.IU;e43;Go sFk&;{*}7HFuŶ1qiK\&Ox# 'd]5e;`G(HI jd+JlR):oXP7?96R.KW{P'bͳМcy}x)H3(AKg4/ɾt*. h".A W]a-eǺ ?2tg/ ޝd|6:{[ Q ^ӻvLAN"+44S~8ok =V)'CGa3:FԖ#拾B.tUަA G³B(a'޲%c;;7٣ Ydm|+i[s7Q>$}_A~;ǧh1_wϤe EՈ$Zh|tsF{E}I-I'Ζ_i.J RX3E4Apy|Tt]}S@F@]a5tq{jIЈ,lOL_׎5pK\hG#+'3fbNrFдR,˸QUsEV޷8;$.0 ZW6wcaڋ1`fuڤH5C>͇*jqɉQK?vqkh;Z%}u.A_9NGz`[!]g`p]{="@pg / uI Ջ`lX/8}a(:\ ^JȌ׈1@ךfq2/I+orJ,ֳrz]Y3@D,huk` pf".e_h*\'#ǿYu\uԥ xu}ޞ8Iª8I{y C=h\4Ĉ4 |e hp RaA|͜-cX7e5ôt{vgd3 %ry+3)4}x–Ha%"1FΉ) \/`QE[SH y ^nD³WC=)A ǣMuӳUnJBW]]P8_PPA%gw@z̑WV ?:?0=X2'BuGS%acUyhDRYnnq/i0p&E)BU̔[W]ul龣{[IzM_I9oH^M7b0@}HV!a 3ia; K 0 +3#xvs?Ai]aSl=f-HoSx)q"fmɏ7qsߓHXu x(DzMx۟vf;7lwK/ٝ=+lՎt4l\UxݻCYR_[H.Յ $cj)by_>ștb8i LC<–Ux|bw HfqI8G_hK#+#D;A00ߌoo<6ш-W30SB(e}]mH$ڃm: "͕ RG&ԛׅƗt;@-P&K= v0_ʭT"t#Pynj))F1fZ`i?QG+QD 4H.PEXR^~xw^8K>'[ JZdf# ;५Wotbw!J7ɯ*!*Rt˅4m0b4,iQ~=o槏٢ȫ{0@߁$xne#䢙%DDjǬϟ7o c,͛g9ڴuw̵!('3$Nq+ lil7VdD8)X[ 0UQxLL렛lJF3ݦ%V@uz.¿Qrʎ%La8|:)k@ ͪ#WM !}oF%4s^RE(:wY'HEaӈ+oQigO'.XQfL;l eNmAwn'ݒl38+X{#wj2{M. gM?oՒ"ݵ-O;J??KÍi6ݚYa{  IHAMh"'{{ .#Q+ L@i#Q5 .0 ܠF227%<RZIDPSWK~4,XT#83@ -:*VYRa=U:^"2e <"]?/7jqY+K)ԶKɖapuZf~{eLܶ] )1Kv7P +ay/h8pXK9 6׶A濏JŇ?q+j׽t't%F(Zud4 5a}P-mv1#0F C=2p@r\jzfpȕSpBS MMBi&&dNɐe.rmfY&>l,JFJ.'G)+z3O>e9joޏGЊ=]@!9|=pƀk.}QE`)"`kB+ʙ\|Z_ϺTBV.2/'C5v$,NoŽ'ͦ f%E4 R ,i5s ١мU }8L󬭩̺4ͣ%!5_[m0oo" Y!3 B9BTָ n'ZTK+&,_BD`S 2 d]Sxi~_!"Ta%`98 dFu[l .,o?2S+C %R+߄9]nA&hBux)<\@3|U2Nwvh#QI<1^X cPbJpJ`-V&}2ѱt4­4SJksE;et&Lrx3~K3;pW{hP⬫ng7gѪ r8b[ۛSKt5׽w%-7]m#y97Il Nn60q4igImʛoM" 'BgY\$J!Q6ĝ.LZWy X"JgnӪX'OOa ń+^1%HVSo\FM.]991Kj0Ϸ|J~;kL2$܋"oD(?-f_<_j'|2>#נ[vH/*QqP=yipVSoܹ}:fzBNAp2~ℐgA7Gnٖ6@VU{ȶI7l'CeHܩ {lx)뫓nȬuuⳠ 3-} @wl'{㦘1o3IJidn7Lv_v?TT3-"Ju Hf,`܂P/0GcIKJ̈i:olYi| z/*ОWu8Z({, ~ZFfAǢimUn ^*.+@ 7o/;v Υ/5 \=@z@\9|]8ip/MRy=TV}oC+@Z(1;4P0R.CA2lVAluD3ð 3;Ktw_ci l_ag۰/3#/a䙶.ޣ<pE\M;)Oӓ3⪮Gif0p-zQYhSByF8}FiT )vN\У6Dg=8 R6i&:*ֺ(<.2 ϱ[[#nb^H_e^ (6l>RJ,5'94rUEL>.xx:u#KFn ^|7-f?JV`OE!ԚM%=/*d޼'6+A7VQOoOAaӁǖMrfq hJr1etCJ}\Ē}RCLxst(y/۝y,vlWI9[`rf®JY~_#FIXD-s"sHdE.}3[^PN*G8KDM=9b~}H*-_HW' x_.HĹor*E)l /4;Mshgb?2G:r!``4ܨR'Pd}h! K/s+pOM_% F 5FK *OT1?Kd*lx>Q0CJ jN|g"ĦlgSjZ#^ILڴQoSOiw畤Q;$@9 5eOrUMw/6k&۠fjJxT-2`e0y)@{   U_Q I~$ڦA1ΆG:vDA4MIj bچ'TffO_l~r GPeTx q*QkM:g?M=Xv~mGHF1Y-:V>U{`]#pd^`y^U>R!QOPyE 8F-G'M02y>tGg`&NwwD/]oXLl$,X姑RF*qD*@۞h17Xq/&: fJ5#pDf(Cݘd5ooa,/P^* LgY=O8Ss`?o[n\'Mx"'萔>^=&4 #[Ī`r˵$^+ht? 6sRfbk0ZƑ{'#,ZA*y*V >#VgwZ2 ! 9DD1QiLVn DYQrEaY+Ư!Nq9sw+E,DzldrmA$M`@t!YS$Y@ϲ^V_!*/wxjZwr(LK{Q~p4WR$Htg4`VKAL=\.CCN 'o3hH.t#Ņ 9ZQr'FZykG.y&w_B MK73wz0>OS%M1M[C_9]Dc^utCBQxwDŌ݁_c7P}eqbB2'+U!sX9b-^I/bUoAR'W=]^Z% ;}$D Qy{KvZdr%y붇x7(?FJvۢi]}c}Wzq6m90[ +A_->Zj*/6~~(D(2{ʻ˪uWH4a+Ĕ]%|}u(9sR 6K,j4~pniXp]SymVS$Z%7yF_gnw^yֻBJ&&rP6v +$_3Zupgؒ{vTi% Qi=8^ؖ4+$5heeȐr or-7"qSAi-p~>3s>#Lx/MNGëmLnã! d8zm>C<´nwܵi\e<تmK'UJoh[ {fOi |gM EnjښK_V$hOI ;wN|> m$&T)gd}@c(3ؓݹ|NUAp , ϒR8J/>'ZG:AyI.XFU&yZ*+hyo&kAd,ݮw.xo+w}d\YǑ}ROˋ6̈a=NJJnC֪rj!~U6Aʧp3JcTM# ui肞7r?v;bY(N}E;ay+2OY@R[ٙFI5-o2=t &ki쵡MЁDۅ<UV;gǰq[T-E&|Og1&Zbi̥@rAN _~ ۼw]AG"mx]11K_g|jZe%Ԡ#k}_זXޖޔT#Sا.w_홗~dA |:}:]LEc<K0sܿ34%.j1GS·cUc,:*j[fmٮ98zx7&vS -/WXQA&d <tQvlAsz0&#'!K(*%E A+k]GG:b gFN\nJ Wm)ϛW!m Vi}\tuiqkZI"n =&}ض+Ԟmqe_꣎6YcWC#'Ns|j.Y>Y3K#5VUp3:h_$5era_LeN$N׽ģDYq.Ƽ]*^v'V1YO͊SJ|ې'iMfz]q; W<[Nd8ߖ ،|yfxӎl`$sRg0!s<6MpN^^}aӞny4Ӿܔ7sB1dY;@O+EC;ټ,/-u'?ק+ß ?\𯲋F2:J!`DΔ:)Cpot `Dw Gc`#/wOj\z28ݿJ,7c_; ;[c]t|G84Azs,{CC#sLF%3v\rݒ].؞+B$9,u+!iwW滭| jsƶ#Q0[Ɗ_j}e F*3.Yf;#c.}NKaYdt2ӝeb|p9O LjWHb) ?Gp+^{կ]?^T߯K\zb`U75[m09Tȴ4/l}a3un >Ʒ4ҬhMxw]F)=CBa7=`s% A%G>1+> stream xڬct]%ضmkǶmɎm;ܱmɎmvӧ}~c\֬Y5k&#RP43:13rY8)\lddŽCg ;[Cg7:P`LLMF(lghafLHNECCB<̖`mgouި :M- r℔r[511 @EhjHh?9c ::-܍h 6NNτNf=p#5v1_#l}` vNNƎ΄YDNnB;H;cJlhaD pw';7ſh8Yؚ'ZBG5g[m `mJ;-4?"ikjGo\j?3CMڃ` gNe>$oE7qF!=ZZ{}~0v21ֆpC kÆ7Igfښ} Ho;D؜Sښ-lߊtLŧbnaleOؚW":S𭽳7YZ!$dNE}Y8ٿr21 ?ײΎ%32ϕ53gV mMq8:~]5;zuΘ'2-3ݹ#whBD t(ľAƮ/-|6qǁH5Ew 2ׇi0A.L=jAfLQpwBQIoʟĵ8w,Zb#r]y#` xMN,!o)Q}'+.bb(YRkψ3`Px GA%&Y_΀sr.:hq?XT_#$T#NK#8mdO>(}&ឥH |Û6b cs_9~=knFbPEŵw2,{%LeM9qT qϸ42Poo6Bځ26. *;̦pzZUZWG{_o>PP4i8k,ZF ac+ mB!*o!dZX$-2 ΋ A!իq3Tgo垧SuԢ&xs\KjH EV4~ʿ= L;/e)`f>08MG UHQr٠~E;Ɗ)|u+r%suemg!F*{pxEm{6W|HFGۋ$ N,apaӇP: n&*7NOr:a$W[8U\$̷ngYg4S"6Αc~fErUp?x%T6t׎nz ah뗅ƓV5 N))1pyc Q;wcǗn99Јi`*$nv%_(LMj&v7N3^4%^n,EzQPc;0vQIN K=w< \dqv?K+f}..G#R'0ާ,s8f9+bFx4n*K+4C a>-=N6O7ZHIL(hK(;%B KU73\8rQ  2M ?]? G0r[g,1\פ4B+ƉeAADy4/ovR^%[BBHs;cfPՅPHHՔ s):^VB=Ys?:=T/x1clQڹ&r5J: qH _Żm'!~zc‰mYb70!Zx wf$&3u\{ ln'ps'1:@aZg6; a.`` (\w(x+M9j!Խ!7i{-شA>ze]Trՠ.ZP'= dp# ڥ#GeF{)@׋$[ Tju#TV+`)- gyϩ%@ 7$|N7l1dqIlfEȓ)Cra$K{ݥ]GnVsӘ 42t<`F Kjg#4^NŠ^#3nH8*TV(Seȋ]\uohnj-|.?`}xU2 eT>~!^Ez[RVvF<>C =+vlT́E㰖_ʫH.y҉q;~F'hT8&æu< Z4r)2R޿Ai-F?w MA@2{q!α6ǻ~"pR*+Ib,s֏(GyӪO'ۓ%?MCАt_6:[^k0_ܑ.ˬ{C@LTФCD?z {!2h1AM|,p?9s!xVTZ|c,i!-W"h$bGMiblo|2SK8=lfX>76fXŜ)aagr<@Y_aAXҭOH}h%*/W*oI !NLSA0VACOGbf2^O1&Y$ ͎J|T1"wk|V۠͜F+NsVoM$3V*>ҫ` =uf!e$ZߑG+l,SJpħM7.cAG~x\#'.򼢄UG=^A(4f ޸]LR)85  A]VW.uBiJ+ ÁTC7:c JJoF4\T5ΔzZy B=GJ*Ywc9g^A3םWp,XD+p \ Gb􇼴zQj-$/2B3dhwI0dݠav\*bpeSznUTys[/G 3EzF{wCeŢmP}/$G/>[R9DL}U<,[)U.{G(Ef]V&pYs6H#$_j).d_ce_'E@-Q3& |*Bӧn:wԨR_+Wt7\W*hƑEX.H JY58Solx`2ʍ^Ya*t n ;t͞Й]X;^8 782.P1( .MEhQZiu w5)v(k?[`&w@sS*Лmy|C:LSGY_#'4B4o0,E: |HZTͶY]8Þb?i\.yc"bn_Tjo. }ՁQ2)nl0r/}?H. 93 sgόY/Nn7j(5FZJ8Q+lL`pIա7[ԅ k#Pi;P,.N?o×cȎ;f͢L GuntJ@=^ΎWfSު)q׳Ȟ[/R ] j>B7::,Sߍ< bk~rIfY*_bSa9=D{OG?tq6͘8,xŴn@nuq]* xo2 sg).#,j[YrhFy)&+M1Nֲٜ&a_~nT5Wz^6B(>GדI.8_(5-\aoXi lY܎OU[Q~L7Xs}V,O+8{g@hBjxVhy:MsQF+D"$^eWk,ɆDsuc3A ?A\<!KED8cpKM1v%0¿<ˑ)\ct\m@ɺaS taC JQ5D0_{1 ϱkRķ.>[ q+Lw<Pt\{q߿ )K{ۂUS#Zm}{E5G#oLSN9SIC 0o恷1Mē䅢ՔUYK#sD}BJ:/-h1EfMO+B>+N y0~|4yO{>5XVpotX\0XTnw'-c)}ohI"<1S-Hwr p/A3mzJ_;X3sCy?={(D)mҐlH!`J)푍n ;jѧ 2˜de>=mXcAjaS]dP$E23^v`._ij&s/prВSR|z \_IZAsLHMȶ=d|qώ3-ZLܝ1,ܘzXT۠lOBր[ /6,0;L0&L5E0 $$_1> .nuhnQVLOjQ"85[X'_oTp 2̹lsYKupd}UM܏ P䐬]3?83Ladut%`U;?\LFwZ 5Tk: TZԥ_J%]^jC(}8`YACzf j@ncdi马 (x% l&%}f]Qlf%GkpCtl^R90Ob.º(|.?ȥGX4ӄ{!Y{m T٣RRkN$2]MYBG kmC$W5bݰnylW`+LV(.& \XgqaE+BsV&5'!=@Cۤ`BmW'"'tXÌcF 7Y^H/XtG*ӝi>nʪG(DGriOӢJ½(Alw&Ia6ÏpltРI*,&\ R:Az]}frhgy Ɠ-4X+  w)SS1vS ̨AMxq`d8IzXiL$|f2 x!vlpg!O J=kDfG3寬 M(2 k*nAZF(%87ף׬`(M3j36)UAu֥H`pڂ:k#U'"Z [1Etk)u$lr^^=̙%v={ɄiʍxEYOndIk"XͧC-f1&i`Ȏb֝,[)w R:ˏeɑ\ޟK<c96rYoGc*\2VaS4%"PB\:Tvg g0us $I<1A5)(+ C+p$ ,V^ndݥt$t5".iJ'{[ gQ9tߝ;GnYVB׵nm?^QgXQK][_6~6+46RQ=:Gy@EǏ0 & QKZCt^5.0ӧheLM@W ZO2*ӁJUz*7!re[w0RVk$3j&&\'(G?]-`H /Vm𠦉(tBOl}9@JmnV Zl-W|2 )xM/o~DZ^;Zf eNlҩ )ē20MUB|95} Ig#Vn|Nii0=V.է@~kJL~XŎ=2 't34pWur;Pkg(. H!A&0,qs,a0bw͔)G:&Fu|ZCN|:Q#"ÎuN \ga#ȧg1>}F20jȄGmaI6{aQzIxjo'ks lGl^'`9FFh]ヘ8jI ~u.S jqQ9PNiP~lȐzv!5ʳ.LӻŶI7 cw1)I ͝rtmb+ !9x3T-/ͧgӍaRzK1`+`}+Yxd*3Rܘ6R̡LxrH#`@__b1 |07}B7F,S$XccQO+FtNшmQ"`1}M.=7Uf v yC156S?y 駖 " r:_#UZw5a,`m+Zf1Bװ(IO"qk* S!+[xqoE)q QP.hB= EMXD"QßVy[7wxu3z y?Nfϡk=ktwtwB_zU6擷ذY vA%h]?g!|HVҵ1g,F"LX>w;l~G4Z"RtIopsj3G##j"b i+<2䱩AЇLuu8T82/>m[YW5S.Vi= iYۥq13 ֮Ly}2o @Uk+Ҁ>NȖ%K3"ςgR-`\J@2.ȆW}Trr9N|9PM.1@%mκ rVN++@Ȯ+tfN˂O ȩ - 4<sd3GgEI@_4[NM9fv5GRDm () ք"OU8lm1gtKGѼ:y)hu̮AJH;XE!="QXH! @٥\5w}ݚ*"z9.㴈 v^tPRE(9K7B!>f03p4y._4$ xK2$;Z IZ(n5UE5;輕#7XĦElamh1aF֠ܨӷj9[lpaql>??!d LP/(Y '^ƥ A'I0ܳY 2^|rv ja0sʹ}Ʌ%|⪹^Oy89](q7j "#D`/;} ,|Q/''DeLt%7dx7DXё/{S]~^}SĐ@-XU5˹hxV&[()X&3a]Sk*Y`ZTcKbP\ߔWD ̛ݺ`r+j1 9yŕQ!kw1]mu=MRf= ?a&+Y!xFDJ6TP΄Az[{VKx*_Ғؚ/JՄ4H4T.n{"R+y %&A%E؉1-Ta}Z@iӞBC„l(uWs20%v&澍\R$4‹?$P[O ߲!d-Y t4oO H ښU̻k7#Lkhg(%B3sﶫ=jS7aw.OZǹ)J5^sL949tyy ZBl+_ +Z<VzVBH~tWd G+։1) 5ݗy'ƵXaj/T󉓀e,?K\rpΌZL_wje[2,cv]A~QCBRW2P#A6el7. vSR>j(,GLSto(u J+PGQ$ Dru ^QzHmVWG ^G ۭoѮmTPZ4wͫy9jHBQ'$w[Pe%l2;Ӵ]SCxJ.I!j$Ҽ9T/0L+~u īr$ =)@GcCL X>(t呟 SQY(J LZuFaY`=>휴HN &ҤoQCŌdzQܻjz~]0dq7I]*!~K1oX>-.H/̽yřHH t6M|Ա#F+BBR]ϱZ|kG scZŪ$D%TUY ,`%T7cgL Y)BklN |:bKX"~ *‘ zZq8(D6iX>6|8+Mڸ%ncF\fӽܒDT~`> V;\V9 e?'PæDt7}yPU9F Bǡzd?=A2z͆#hyʔ0/2WU|Iؓ(J*? x-N.P~%W:}Z4@/w.а-D.7q|2~p!1gL/p#sR][,.Vyí>[rMbdc[!>-!11H{ OdNjѹ_L~;zgZؒ`zP:7ٮ9?qe{8u165D܄g8/Þ~ RS]9 &+vW%RxB]R鯂RbLiەGQR(by@NB\G(E{@sS%kٕ59lh){CxI zSaюYi\hD&uQHOX<%Q]Zˈ G@E2K8 ZT'3]EJ֔"M>pQ Z Wȫ#P*/&Mp@L[Y4;8U:HP⾍Yϝ!2|臟y%y g+4N:nQFDH0o˖HCCMg"/ ݸP3v9~e8`"mǗ qҳ kQ,}G$@Cn^m(dp0v"[l.wě] j 9҄`vI(!pރ)6߬Ltpb]Y>;kdN5hr(g/e(Pc)N.鳽< wq ٨ǔ Pw녾G^1s6$@jזSBIVAq7qOKFs  S=_9^m'ogV4 l. Ehj}fP Pjo :!qC#c9y04b~ =tG ậ5g|uB:Ws DCut0+sĖ6e#~--`!qRe8f rzn<BӤbMQoVƍDv xd`BU~NJ5/6;LGTC,c@&r;|a"Bsbc"f`'G2rU ~ϫY*/w߈fR /WAOSy)>25C.qyh@o)(<ۜ<h UfkY]UU `rjyt@F0x2=/N w<lcMȢТU?Ɗ\IT+Q*a>:aGp=.VCI`!!՛8/[;f4+O[{nEZ4ߋ+U d fЃÇ0r[gg$-< $iV2>)T$#'<~Lp´ۘf5q}p &NQ2!gB*XD~5*h=-fi'$R,iI*t@Esq;~{S\a!|#HԀy:C+”E5pq˝" (&nFV!U3?BΪH5h}5l<qkD1a'7szn9dz{&'Fo͠斫kVͺ[R>'#AFӦO4\Q . |j*%Ky1M 6]j'-?y>1X^GoطE*{N~rn/E-z; xȯ[u22dԽd&;t (Ÿ8ȼLGZ@W"&9R~kj [-|Gd {3fZ B\Un+xژ9\$9RP5%4-,+r^@KGDhÝpbfК*u)2c H_LKiMاpT 2^KῊ} |8s݀b)?3~VUscAP=N+?3?Q%bhɛ30Em-٭F!ƿlT{ 2MITё^7'RQ(-B,yެ.`VEVgd(IvTNF ̖+$\ҞT@Taw4UJeڳ<̒.0,t>d/F}˿6PfJ^.br9>!Nr5jwaR;&E:-\2wPܡ"fz /BIc%)ѐ?27߲; Q[iR"DӱױBEֹ"ݖP%&m1-3[=ےzحϵ ewHLɐ`8?M\h>?rfsSMnB~34jqcN;Rq4"Yc [Vک]v@1kZJF78wfgHQ5ᰡtJfv#cҀ0[qv491+W[od?У:<:eI-;WH(/U0czttg ׫쯜,~B{8Ė5*Φ:QUJ0yhKug 罧@x]^:U ?Db{|yI_|O6{@,RW@$lmnk;i=I+˳v^cx7PUfk"f6p[Ձe % -<1m94+eP *MQ.G,?\=RX.E**3dFh},Y.)"Vl̏M]81S߂# jPqw*^HF+tEA4Cs4Fjs2/Jr\=6Chm|e-EsP,}G.74i[g \rب')cPAt1gl1lu/'vr$;JaOTدvIt!ŪTZq57CS&{Ml;,Gx8pf5(PЄGd: !면V44NK!Bbec7// c"tRYUӓLQN!lQ5Mdya߶s\d(|;O j6t//- ]7 i:Į(Ucs_+W\o%lܮ4ߞauߙ&,JE A0s/hR b0;{Màl52 aU@שa Nx~(est2{g {>\&a%(پ'h4z=xV+؜"wuRu h[9ӄ0dzFM[?˰=d#!`3'8#nr(>r,'# WC=\\qq8*jjQ~LFL&x:U&4kq _=Emʕ |EBW_5 2Vj z "Yxfxi\ ؛{xDÓ\mF[SXkjFTd֒_LJsqABثt1#AnӻǻT#ңEzaMsyoI~~ #ǥq_[fMJ{t0`U_~\5Ox/ yO\:JҞo)Yhz&@HJd:>/={2-'0\P%/BܪO^;ly endstream endobj 739 0 obj << /Length1 1630 /Length2 14450 /Length3 0 /Length 15301 /Filter /FlateDecode >> stream xڭeX]%Cpw)ݝ ]w'hps7fGu^[>TEQ(`P3quVtW`TZ*Z  4HMllV^^^xJ'@MKO_\&tPp:8]R_K WV*$ VH c[VL V@{g -``ofOkLDgG0)Y9;X9,@.g7u5vsraKl rtͪ"!:],]l84s0uai.V?L3+gG[cϿ9UU-Af@g4 ߺ7vtWÿ+g9<+ߜ.s[X3,V\s5 vof39<ߔ;DDyo>wj)W[[%c Gq(yglA'W/.wXPD- oL`nlwfkڛAVkXXaiejcfrxfe]Q % /O4ڊfyGL `dcpssxXY}E_gEc  +h$Muc{`SW/q0R7[[_+PciFQA@CFo[m(S$G‰H-uw"Зm~?٠)T;r^aJEkA d;6ܭ 4!jz])U`5t}nJ~cO#]6j0j$iJk AџPCAָ3.yl$~N5\O=Yp\Qg9cǕH;O/@J!pARDNƓ7צߩJp 3O6FPs>l‘GkPrD@7v Չ3jiAF x}}# Zg׭ؙlz`# ?t{Ȭ 2,|cTKOp#z߃\Cj}4F*MO'Kd__Hh5z]nۣH8T~5fXhp aehz*l"{f# VeFҼ~\cjQ!:>{*'IMhCM\r~:M[0RȪ闯*JiXIֶc Rp^K_J^05;cxC9"KVWÇJ[2A47f ms]mFTl_0Y +JTfϺ U?x_5ŖyRX4}Q~T+ub2 TZ}+ԩ\1ƥ#8J::];Pm74vOiK_ %׼y#@3t^:!e G!}+BC2#}v6IuɊa>e564`)c~'"?@GWN*@샡~Yhvu_=v7}eI0F&bnOǝ󇪡mʝޟY;fE=( Afz.U3JʵrHL$r @Ⳑpn[Lfu[V W2Q]tϑSHhϳ^<;N*)Ds/ؖ GLhXFڕC(JxK]^Z<g. 'a1543$#.&ͮ~A`WTq'Ij"br@|#ͥo11WD>- -͸F&r) ޽F)ePMI3"vWi&ӋgچGId!(MɹF`ɥg鱱ޥ᠒ˋᵅ.V]YO%'Wl¦3)ӻҢۇi:KaX/y>S ]#ߛzNU /dLf<1Scg6;9Zf{g>e4[-rW}cݨɒUp=FXT4~F:cɁ^Rx7hHFVsf :RK '49@y}}WEH^Ap|[G5EUT!ՆC_0td0n!^qp#HeB!Iֲ-DrGi5mp:4H_~!TAwL\D[ؾ'FL5']ŬS5}6w,9k'P -R KI7߳V SW9}_>rD^p"HG5^9oϱz0e"QϾtrdXiO|F$Ex\U<.km;Ycg"d h@;\zy"X˯`B ;=)tdR<Ͼ`L#0T/6uq&i1wWԺ/b`ܖHJoSnd}ԀŌb_کE| N><'JR>1 * %S$.i Nnsz!N,׽4@98Yy]}^ ($s^d%{{%3omr3^``P˅@:xٹ2aˣ V?=p#fkHD6z0¤yT4V;.VuJD_i$M4QM~חԢx.0FpLH[LӴ\g Ujsecҹ߂թͯԅ^P"#xCNFl^%!9nm|ǪrW4 p{pg+3q23!HݠqV}SyiE*o`VbV;-^Vk9%b 5s\DixcEL̼_C.ܺbFoYL *W $33mj; %!r'?yl ЎtA%X1Q})Ѕl A򔷒yP:Wr7û^U <礅HygXQ~%Gg޻?I@յܘOO\.Dw_:FWJ̞B f:%?I,8ׇnn0 ˠD~Z|~$ž@喢z`0RB\n3u}CqJ?} sz5!"n,q2E3gf ^Bz˨ɨ }=RDtKە?7!mcO򆊾)u:mMr{V0%q:ot%4/luDEb,>xcw`?O(B #,2̜EY(FTEKAx&:Qo3SiHfC.%U7b\4深/X<ni<sRݟ+[.xKr\[~?FGiMc\ Y3D*ׇB'I=͘?˵l xT,TMXRT\*a싹g;R?"@RK|p߲ceW~>=OeB穕M7uc#}M@[e"V ڑ6Kƅ7m t`rS ?lМŝ]&b-7$BKwr/We^ GTQ93)!0^J z\+JM⻪a[ȃ z.޵N%ҳ983D{)Zs-ёji5˔~}e h?6vGZo06%%YX ]!Y$wN^JQ<]-}{f Q|d!Q$9EU(M,ha &UU2+’e ^$B׍1jv0 B1Qenx;YX{eo^>R\ZT9d~ND& ; Q4tK}! }߳xv7K*5J(Pwt^5Y-:"c"|eE_2{%s7B"-NMoǎ kkFRHH(W0]u" 3?'<^DM͉ Q 9/Z¾̥0{oNYHki_Gw\KCJW_ _DWrf%षdDcM}z4 AK1¬tzѕUKLzw'urG(6p\6|I󇲁dg8+HB.s笃vA7=503B\Y`eW})D= 9!$'R$OFxH"ՏnCjPUogYfIUiή"$"םoi8e|m-S lχ>/ WKZ^3>}YٜgDrjWլ_c;+?u]V#46~)*|pu)  0AehEji_=Z1)м߳`v4┴f||AȞ;~ӊ)Ve7.˯P~ i0Wx.Rrq#ީ.D$Gc陵NH#@-w`U}Cyho ّiNGeWr <H*zPRW8HkgriΠNd)BH B!3ix5I|[P g16֓jA*7!&3 *{dr}XYڧ?T ["Ђ9p@0Ω*CeI5YδR=3k/|gIDޯBv߅EWH]??BkFtA;]7l:,|^@Bd@D;[C̍hWkYꂢApË2d!Rԇ7n&@=Nv3DQ Aɍ1H5s>7M>ߕX2vz~`Wqwk5w9Ve1R#S%nE[9 C#q -UG2Z&ahQxς[*T)~H gɣzlHWTp1sң kЯn|lN;9̎?c6ީ̐G%G R;UBgo7`vA%`i):cY b{ځ4?,kSaI3ݏ>jWςfpZG&wM͜zg`]w[K1 S6QU?>JH*RMnD:2#L%0,OHgVKJkʹ )k6ߦvPKB[@Gq(An ;h;.E yU"E.^?_-:3.^cVt9=׾v8dHi a~<{F,V B].]{e!3[|@يnmVIw=\5XٮV8'Aޞ nF7!#= ETG~w͡ eZÖ4d;B#ykWUad 2 fy`.ɦ%ۢG]).v`DքC]h{Y Ux 1~{w%,MUר~mm_$(7P?alSO蘢IA~$ylcpغRq d-䊆N~ 1cSX]`*4|bJ,U)YJ Z2N1ʝ;؛FTZ}x@l  Qpulɠ7g;7an^yH-0ӖU}GK2DGw/G)&"JDeMK" n=%CHL?rCX.>ƗKJOZ١.mZ yVHk |6s*IX~҉oiqyʗO Ftf/ÌtyK5癄35kyb|ߌ`+e^3W²݃mnyD ^(?u:F_O|ɏP ;p)ŭbKR7>!L=X'#N*Όˏօʩ[lCcb^̦_' Ѹ>Pڼ8Qx!Ix2v!J~ԃ%74yj1|+7nY/Dtf#G s+}OeKUmg\hPJA 1[>%u,u+kj;3Ŏf%~ SaUe܊T}w Hrjs&`|o} /X14CAaVjD 25gС^}@v[u+_R8?R1qx߹d9tl9{kK6|+ȬV<&ӿl" "K}Fם=4j MP7ҜNjwa 9 rf` %jra,4#㑓5jzN76dH*0'L+Ç2`:pAlML+&Ldj.eҸMz+X;gk@xQJ*l׫74ϳ VLw\"a{P,ՠǏI fBt8~騵T>pГmUrh)ەBqғվRG*Sl{ŞD =_N荌_;Hb= ԭ -Гo{U7 WZxE.0ٷ8$SrC.o6g P~LFqC)&,:*TG\8fѫCbDi?uQU /lN *' :`e ItϟiߠӬI=fKuŔ-/T.-jYvhȾ3<2\4GhlEN5F%!R~$xb%;oc`uc%Q]O 8.ݔeOM(Lv|+d:^mwevc.mx )g$d\lV3>JR'&]9Ԥ;{OG=1w {Ou vHmwl 2C*I3tb4җ>j"Nqrܡbu1L(^Ҍψ3+UPqa0ѱ;; nLyD;%8GOMLP%%~eY1KoίPaG]tu'd?\;DW0'/(8X$exEa)ΪsMrw_2;revmޑ!A"&$ `*yAEfHo]X(~AImyl(,8aݯas$?F9yM$SS uW4O2mx:B,xZj׋?#.K5 y]J6Dbz]pP=-y !9@m a`ve %ksRԽms94/NPiUgu$MVu)ӌ&6,Ȓ)O7 1oξL r2!i q#*)O㠄mfvUVR3{;\4\yJ50#PMIm45;ABsE\KZ jP¼V#\85W% f{}TP+ژĹLySiÉ#\еJq$> Z/ h[ ޤ=.ki8Զ-alf70L<|XO0\biy:=}Lt3bMxs88d:@71\kMe68wXZH0U$@S1+&x  [AF AK3L*uɮ ?ÔHWb&J3F֭7a_Ёߑ /+r#ѷI';PJCn.~1zR|ygg~K롕U!t"fS3o-ӌ!o]חg-gh̩QJHnd1k<'ƛuz6Ri˫%%V4tt=F}VVC)$^bl #~ .ԟPZ$(fTmӾ, oab:^#\|ka kxJ 7!cs(@"hݤ\ qCKΟ_y C=ĚX6mk扅>fa茽Dp9Y>o$ PHc1 xDMGb Zvi+Ɛ0tWrPqkMΉ\[C ã" ,Bs۷*c7g S"}8K04J!`T#}Ͳd`cl!)3@#Iz]U?;<W}y46V$}|BX;M΂)Q7eqx{ʱEݚDp(Ⱦ5;j:?dƶ1^M֝;y^T%*.z)bE*9"|ЈZa23z G;.+rֳ#B߷X}cTÔ焿M<)d`ƚgO=^){0M{0P [ԡnv67wrqѠ]RA!$1e=ќu~sj/:/$4Ua48 ҹ1I'蛓1*阍9PԤyg O/?l[ ,ʋK[@M?rr&ޔx=nE}px:& ;WŠYy>b%2R+4~t"kbdLʷ+:eѢ8sG}${|A7iA",r!ww1gA*!L?S 5>}m»/?¡'THu8\3(W{hSo"q,8R *3zVʅ;~`So3?YIh}÷фEqzm)4E*UH{[VuB;ro P W?G:s-r Qebg:@E#vґ{0;7Og-?"%O27J( i(R5CUdsi;E?W}˽B #No'̄8?K/Ex H랫 yaTܮْ\J<-FZ`\#_ 0vȆ}0Jxm4f4w 6TWd‰1I|>ͮ+30AoO8ZxQzn! --!2ڜfR㝢V:L|}r犋[}TI 4C 7>EcnU |p YDz v9 UYȒE{YPr,󃌇%u;Ȅ^LfuGқ< B - N !0,P*ku #/b~mjwgf^7s} 6 :T5(7v+b l:=ffk.KF&bN0Y+wv0 _crV?_D0F)(EJ7AQ<6om~X٪^_6N~DϿsǨ,-~i rpu Oz!ߺ wb*)~a޴BҚpʍdxUOypagR)> stream xڭyeT]ے5;w~%݂=ǽ__cWUYjZ猱)HUM퍁v ,<yK[c)Ȏ@A!4r3r41 ͍@wpw4pP)kW Ks;Nj+h?ި-3K @TAQKZ^@-)l.6&9KH0wc0340`9M,?L@'[K` ;9`igbb߄?"l?|` g3#?x:[9d؛}DڛU߾ ts+1`j r1rd7 FN6@NG'Kgƌ#GnsK;E?}@D|02q?Rg*3߉ 'w!ߞp7\0uF6Mjv6ȟ0Z$,݀&3#mW3:X?vZXXt]@;g!߼e4U6;JCugUwbQW{\!"bd`d01s~6f6n&@,Z5rvt|wko0v&͉h/Ӈ{ȁ@7 oUrZs5vNO @CQj~_}orwKUc/c=.,Dy7MwuVN}&"Hϋ9M(mf1%e‰V6'?2\? {O>&I1m`hy'T}ÃW{t1F>?I nL^1Td 0>U#vC Z%m4̉ɰNġ7vuMy.6fj$I}H77 gz 3ژ4 rBxBcǚFvH oY)⩛sh5^OcJE,ޭ ܸ42潸mMtl[*޹pb[1 m$v@y^f^iu\ckꆰK8 lk|[YJXw@>ôuؠwc6ey1?1a{Au.಼<5(u@ϔB'ZNX26%'=Ѷ!{p/6b95#pF**g[$9c'W9 \e3RO:€.r≊Hy)M#kkԛ$qbu 6c"HEcˇyUͅeDQ\KjBbkUz -r#^,['zd*N틉3R5-Hevr/cSh [fXEEAxΈS&ѥI: X9a`OYJMN3'`]% }EvvNyV2̟ iZLÿUʬ{ǩܟԓ7wH&+[O8j(0+sA=y珪;j.e72Xw!'DTrY tƧ{eRXFELj.B`"l]^2#w׵],49ga!< P8 W LJ1M|+Z/.8zT7/ΪSu -z&>9q {QD$ipiz C| ?z&HK_+z 5N Nnwu슈6.O6]ʑ+SFdQg=cG:pOCdrוN󠥴]8u)|nTiL9= ="ވJIx(i$rBI 5>o}l oo+򁚳v8 pa9;E\BiJ.OWכ~lX-A} C_dEH^c}`BܾTN3bA8]OtI?TDRuo6atz:p65̺-03'wh%~taP*I3 IFZ#GѮls}0r7vn֋= ņ[)Ui_˙P RdR#KOCk kc!4; \޴؁؂>/E9l 9!)K0Sf?AҨAYJVC $-q.HN]}Y=BA{\ Mvs:!@ N暥yтZiHeVtGSIRXmP4`W)^ FrPptUaR#9I1%=s$V[1n !Xr >a bW{F@JDn_$NmCRTJ k-=aeQp}\tqe)v,A,%Jtpo\<.0zYgwq6q )Yumţn?OLW}KwHސHUwJP_Qg#<)gVt%R)#dcPuVDbJ(U6,VNxWm͓ tÔSFF b'J xZ5Q-Bo&SiGmVPvAJ 5>1J~[mR^~%Hk#knuZ1ჹ/vd89|z#*kFC]ǻUr<]PqkZ{^ qeXNb.[lF̂sj dy AF1mE-|4?f 9E^) G^V+~!g*RZ>,FlhBʯ gQI=En m5ۯ~_zϱ <\r3+d3^j&n(NxAKPp\>_Q8zfD%p7$voVe'i@G)yѣKiT2 Zh!/f[ԍ7J)zwCöJch>MY1elwb{J)'}!?+jZJ982ڧl/yj{pQmJ\)3j&גJmDg_dn *0Sȏ7b]o2}o7!/, y7z'x[F@YmNG]sT ; &?ޟdr闬Mtm.@3??9Ue>;Ϥՙ_n04хy:iʨoVQ̠&=+LG;dٺ$N{֍➚qkw[4-`<_ >B }V%C%}iYRd9`wc 8up} /RcGYbn}s}zD S`Q?UN" rf2,0q鰄u`ױ(a]H] v|COk=o@RGW9/n"regamp9&Pںw>/ǃ/{@rbi01P’jJ*s$gOH0셡d gμXvi@4%E/$œstGw]L:\ΉXx=qOܦy+dx 4S,t8iɫR#bȁ~vRza[V*dX]]W? CQ:-l^b?dWRAF올] e0`t(c*-{# q4؉kR4QbqWM=cFS6rW j : 1b @L#Z+c k۞n9!;7͈i 4YNT)"6X͛OBV3+fckv .L31J]G&Ecyseqe HtGz`!/-\ga ?u+sŷ Sy|*L3]$J^w+*-^#Gr֔=NZ!Wmn[ {,<X1 KEuʐW.+mp8Q!/M&Iퟺї&fA G@g Db`6I5(ce巤&^sݸVRvtqqŨݛzPQ9Womn?(|q!B6% D6(6Wd+<ŷ\.*:OVMmhJ&~T,_%1)4BJ =@sga$Sv`p^):6Kۛƈ2>ˏfYĮ94? PWm|_Yc>jɞa!d8SWd㡦FS ^UλpSi* lpݽ<V@oiX. h(N/B_Ba_Cp"鯈Axė+Ưȣ%D80[HrTyjVCo*0hc/8+%@͛-YuMŗGY6 v_ ҷw \~`ש:SsPQ.f-c&l6x8v9~-mد8Tb)p|E_:;O;ڝv @uo`ΏY՞'R܉ ԬkYV L$OܡBjyxBmp[-_%O(DaX1uCj$j0h~䳃FT%k.+VV'#x]lr"Xy0z VuO!5"*!f?!(=Xm ǭp0զWQz]ZLDz PЌ3Mn6\oYjzuC&X686Kpݼ gKTGz*Yʗ>JKq ol{ɊȢ?mRxY*z]Oh'@ڣZw-̧ۢ+eHvRNR9jR#Rk$ྖmMwq9/, 操[0c{QEqU,:;`@M6t~(\ل jiJ9lmL nbN"ũrt HBarT?6f#̗hP<ͬ|$ Ժ.lp&5r,TKybn|57ht$œ5Ë?A/(L9౓AɲzN]U4d{ XqN 0s3'uo=IQ#pO}CES^}ƀZT`; *\_][&(\7ȏCU BB}ғ%vPm+98_mmIkXD2|l$,{[hE*.—$xBuFW :?~} ~,US䒥9R/ްԼ{Eغ;u rû!c-RD: [k8}D C;Ŧ ҥO(9RyzUd:]ks3=2@hRY :TAEQ->{&aK%W|LJ,pks^HTEHon=q~2 Ҧ94-3Q;WRJ7Bjgϭs;+Z[՗(Flv>9xc ]+YKێ-QXȯ w܍RŅH#E/r7q"٬= a=9"OGD< Nr0$ %$P>lQܤ)ׅႢDϐNOEMb>#8/[5ER"%[ɶ8_z:s\tA,WhpI[zy村R  ͧc9]xϡ nԖ#* \k}4,ie-2d//s甞YFamu6xBHڌhه*UzO=5wG 94/Jͳzވ/hF^V.Q<9Տ\. [{+Y jSgj|əariyR[ہo7Մk2+'0h!}!NQt{$hyXo9fc#ntFWa+GsQ)_z]zf |e6q< C|+ȸMJprC3:oa}ׇB!L1U64{fSHl6DŽo)ry yV>.GQ@߃K]S8 18󓊇Ep}IO_~Zh[ =M W|I#RصɌ]Q:@΍UCl&bvI,,h'[&ŴFųPHMŚ5V25UV܋JV2]kE_;Ezu.2B ^*S-RB'A2`k!GO]:&G T%naz$(x {l?NVE٣wi%iyD`i [@׷R[vh5 f;vdS!N0E?b?JgUqٓ#Z❵;?Ɔ~V! duIr@ОGQqY>&e5oxe񄀛5ᢑ#TSowshӂ(%\U-! #) p9?! h>{;xG3 q֦ڴ^)'K-d.Օ}ݧCiԋשn?YqR4`)yJ.yOBP~NwHb"HQM| :KmSH>-u%pXny.K=61wp3RaoPk-aCǶϚSQ0x`@|dQoNVw;仌9!I )=D^ȹ\CXO IԚOo&fY.U/ܦVv5=8:/{>g~=N&mkIQt. N}9S[ݖoҚUȴ]5V MeYܾf5ͻp4,RkN~+|,0wA]Ԯgϋۚ/cOpp7DrEOeMU=hgvC,J&Ispbѳ4LmH5*W4i6 nO\Ў ֆ$N(:^"Y.U " q|洮1F a=ȅDzpK9zQgϴ 7T DR]Cl +g]i_[=\$ 7igH 4&iEfcFQcXƵ|`n#WŤa Z # IϕB(nhNly9ͨ>'n VzT5DQj`g!/̀Tu#a<f V-HEc_z'ЮVħtSfj@8p@,3Foe ;P(yH(S FMLwÀ,AIĩC̀6l*T (\E@(x@slCW[TW܏qHR< MB~|>Zdѭ> M9-x4>ALjg!Ae?wwqk! AUҒ&-/0,Szۦ0x IL֢HƝn۵y%NnC9\[5qz_0ʀAS=[7 ?-Gos&s };xhq1v0ryMUL׆LXYRo3iVڋ2%~|"94ך]xF y ı_0܍S.k_f -H褦yΩ )~z'- b݉ݳN(_Lק`\g1Y0Ef9Gԛв/짬Nk`G&¥2.*Jx]mOU|xJSyQDP~OJ%0xΈ_q(ڔmQ*>K(X3(E-Q? yJI^? H9En27Nn7`=ŝF"zğ6Ima饴[+PE?9z9gƗ! rW?t[̀b~6*C9;'ΠiS endstream endobj 743 0 obj << /Length1 1625 /Length2 4888 /Length3 0 /Length 5691 /Filter /FlateDecode >> stream xڭWgXSkF {[HBPBHG E@QH"RKG@@@)Bsg<ίcg]][O"kb.ABQH$PJF ` rŘ(D BUAA 4 ,T ` 4`,xͫ 7 c"f֢I.T K >%PdžP(P][=c%@ I"`! Eb7Ej)``8 z_@o( p !`Q8\@~F4bh7@jgXw7N(7&H7F!X` _PFp2o4w8W4BCP @Cྨ_y%{7[1(M*P%W/EeC|Aѿ $r13 @@nWQXKe^ -4_sޣwjm_E? e0CŞ\,8ܿ15@/I:" S h8p!-DBh %4w= F@0 w8yQ}!( }[5M#yC :*$ q )'sܔ 7>:hx^FJF ur\ 9쟂 FCPqVGVn6IϠ}G'ƻiQx%5,+.U1RM e}6fpkt3_nV Hi2MQyR;AS3SrrhCp~pFԡ$7請76 wt~'k_O"x sAԁ%+b;ȥ qro;fݑOj՗qManxXr7Wt~כz%H uaQm0YRx&f! 7~!l Dn&rhYՎeicxHfnfyl#>:evA O>SbL-x p@.3_9vf߽pC7Khfd Y®#v T '[jmڔ B/_勎 Q4lA,#>ɇ\9OirtN|2|ԱIuJ3i[56K;bǏT$ۭ2t 9}ѨJ|7kc@;LXߔѼZI\SNZyAd,yz.L3FEBarlH`~:]^xZ'~®js*T4{E wc26ErGN,j~L'Lآ+*$nIwxw+5q2][]6"w`) +qswS7* V~R WgdI#)q9if{FqXm*irR<10"c$Pn~d>5na}=<|u b@"B;DD>t";T|wt7Ŵ%[|\]@(2mK<^ĊUcJҿb4beΉ7eנּ2#*h_J|[ fMxhY]Ǝ.QS40M>EKݵ.IJC|Xy>>8LМ4%ԕYYxkfmj{Vڷ*/ CYxCdiZM7k-d[NYA[WËSq'<,ZCeyַ)Ld`jfq?VZXO,ͱ͹`G*C7Sǣ9FtMՐp.bUXZ@gAS`6)f1:Dp'X䳎SAbEdQ|ZY݈ӐTrsXn<$SGJbe9B A=gЖ\*nO[ǎS: )4N=nKgH9, L# Tg ~n/c/D+$yh|e>;.xỊL "XeWѦg[e ʻ (% r=+q\'KB61yB_o{Cԋ,_` (Pf RXK4 (Z22l fHb ٗ%w ޳ߊ{w5qiF Q1өKK]uۢqӰgT1plj~j?> %:|:NUtuzW)yq^n-"NSQ!O|cX"n36|n*NV僺R*A`( e*t;Y,;%R`TK~ebtn>KJo9ށF̎i^[gȌo]7:M9*KEJʅc 5>"돻WA[|VV*_JC+SK~,؞ fIZer:Z?`|g=T?)Z/o/Ǚ盪Oy#'4˧U*Mvyϯ+ d1S .b\!y%%!ݤ g"f,LeVLib{˼L-䏮hfgb|I>)cI7ud *{Aυ*ږL'=T^vk=Y38!ՔWOu5Ìbi"5s/ƹ+<:=ykPjqƬƷ+ӴGeC7hL+pn;Mʎ_|sbKB`Owg/#r2i.Ξ3'#}^8m#Jg#DWPD;UI{٣,J8SȄfnn~vcA\\i0R U4EϓSgL{[*v eg3i[z0d;Wފ6IUpҌٌZoy6|STY&SgB$3$æLDl/}kYy2t4"tޞs`i&p`2O{ѹKdț";1]$xye@Hwt{u{Xs&[x;׹˕j1Ȫ!#bxI&ϵ0)β0gOL_TuMJIcycuj.f 8ҪOFŸD32q͑Y= l| m}狽E5YEFkRdh&"pY:~=["6YŨ)xE:m%ڧ>--̤ >j πN[WKWth7%R&#;ClZֳecA4$(e=|:fr}I?hM(fY5D o?p5ږh6KPM K6} և(K3*Kd i LhO3vM̞{m[u,F*j,"eˮ=e T{ .WaYfA~ aljuVZ{$-˧ .[w& ˗&!JTt"J5m&Kb į>unu?=%vLf/j>< u9|Sjf ύ)y@yB~͉gn,UHKyM4&#BpVj%216Q 8)ߣ\(7>Jg*q>Ll5v&v+ %X q|/fD_:~Vp5Q7ĚS(jXXVid>}vB'Vڧ?4E2uۘm6Sp& :&eWX!@mq$@CtOOيXa+Hj}aQ N,M⟴]N-`O^5U[ ԛ;z+WT2sl;! @L71/G@[2: Ng=F^9FT3+jKΏy uKءOk!ҼLAi71^_ f^׼ȶ`bҍ,=#t[Ë[wuz$/I4FDђkAVݯ;b~}պFyD]pҾ ^Q.n$'ۉ:bJ[7 fR@w*h! oM#jG\KK~QifJ+w_iz$8 _ER؊~EaDpg >n@~S^sA ; u_4' ~٪(*? iٟ_oH_1/yrW cf U t]ERA9Y%HÎn*+$DkY.G \ʹOqz2b3yJ/ QJ}uNEeyS7*5_v:P4+Sep{.Ǫ-͝߁ajĶJRq[fW 3"Y N:.뱝M7}xA] S#j"N endstream endobj 745 0 obj << /Length1 1144 /Length2 3765 /Length3 0 /Length 4515 /Filter /FlateDecode >> stream xuSyGR2Vs*hbV8-9W M5a|G{y>!GS}n7}v[LEeA sPlEZSyKǧw3=aή=tbG'gҟ#0g F#(2;_ k?21 ?|`}۪%{$^0ܓ ]kV'UoE1bE|ۋVW.KuZEJ Ok 5A"-X3т]cߣ3_k9SjU84 Kf.7Wƣ8X.xC4)ZڊTlsUF~l N_oR[WkVL2wX0R[O]>ph:Qxg txEڽ/}qȕތt~N˗"EqkfؒZ/ZcUEڒA9-z^^brxq1mUR٘uk rl b/L/a"NrQ{]ܧ{B0k nP.20 S$2v7495""m׮`pIJiGxf-|L$Vx˱^ euT68wDT2!sK|is^/Q叻Q:@NrϠJƐIBG.@O]n6qgxȰ~wi5 lcqmly Ԥ8t>$..t<9NPόR<:Λ$ڬ[/ _UKmH-(oLNoxq%@Y],۱"?i+al=Ζ|O-|5]%ufk٨DjǥWGwWw;4JeNOwoJѣ9iǬH)xx-1FSX(<ӧl2QSk(;ưS%y|~X=%L͛v2Mn۷s iP1^Q3!6ƢZ,NZ'DqJ[KFx>ӅSykN09$ޭ m/Dz4 ܵ>]rX-.ioULw 4o.m ;VEpID/>Bw<`):@uMQ4iY?_?Mh46wu~fH8S (H:DN\Bں0gq3% FWե  볮GmV5ks4?nU`&*;_6h\,(Mw<:w6ԥ3u2Bm00 &Ya{EUYbmtÙE4=961GK8Ipi> WY.1.P-voTAq;FѫFOmBa) 6&s˘Ⲉa .v\f-o^-9JW/o݉ {x^ _O?XkV'68zd0Ǟe=A(x.EA|`/+uuxQr2em4`}`Ƒl 1o x-Cbhݙ;qoxHlD.E9u~nAɯwZ(/nZUo@Jy^wP#mݏx+Qblwe03S|uWap>r9qxn fn4q3'x]<5e׶D9l&$}jݑ9a}+5A',DjNh*Їr2)EC}ħ*!UKLR<ׂs7,?;f`8B7=j,, NyJ;6̒\~WBl=JLٚxl1=\TB O//Cb{'3,2xzK^tIqu[w"t f\gN$X=` ӒCm Ay%Ex$SH &tXzk+c|BӭC[٭7I:9?uRdgU8]`dKTn2,C*F2$o@͗qf؊uNC Xk ӄknFtڞ(fr>*BҚLYK^61dn>E:-m9},cͱT6TLvɞH;\SLdOآe40lf{ݐ['D/))bܓc}zRI󂾤DU*h;~cB5\IM<х1޶I}˞7GE_3gIgNs)3}:a䤺5C<~u5+Kyr3[Y]q.k{ۣ)U]L4' 5p S,Z ʚv@YhnN>U.)ur,į6pwPzW~C>C ^Q7?{C?UzY"Y=bVKgEى)Pz4,ť2.l;'qqjُ_ȧE,瞸xy/V+%:mJC22 vfO\˭n:mĄ_z4.f(_jK}54qb馮t~ܱK> 4@WEpi\$G/8)G7˻L޺E1Apc+ڨW朒g&Vo/Md~jnlz񍓐7QRԽd:bz'C~xu"rU쩽G5gO4Qx~ie/rVpf^}Q%<'c$~XcZ7K_Qi FWGQ }?\ okEW'7I.!/ίNL)x> stream xڭxeP].w .7qw ܃; ];3sΝgt?Z$*¦ c3 3=3@hk q˙rvx QG3#g Nٌaf 33) {G3ZMY?%=SP~ـm?BUΖfs@TAQ뫼$ZR^ ifghdPt1d&fvNf4s# WiN F'{3_/{3G[;p4s33q1 7 {GЇ#"h Ȫ(&ΖFv~ KS_%u69ܝel0:y|f,Mm̜>|;ߪ7a:;٘33|4qmgkVڙLQ534 LAv6S3sxFyGJe-[#_9o>kh y#ۏǎ|,#;ǞZ4.-f@+1WWgY|!Inft6|o ׿[ gfb%/23WT QNZ\Q[X*~ (Ed_ˆ^\zV6揻Hw 匜?% Qq636qqt Qy33w3K `5Cb:}=C!yUn԰-r)V?{Ҵ#=X6Tf>d4yhtH?N5".d7!9'^ Ya/h\1}MRb0;PkON)Fzb(x}NH= oMޠ]9F]쵥2ę6əWn3Uj2l/VW%E)TJ:@6 ,ǦB=3ͩ;ihYt ߾Sݸ/Nnv2RL;L ĚS ϫ(טKN ~`t63ʰ#rs{ybԩ`GGcJ%,2%.1cJ5=UW%RɄ0ݖ)Tcyת1*} JΦ~)\K5L-d#_{đF9ޅI>,cەFb- a@^yz*GHV##쟈:OMbC?X3_2HZZume6 .dbyW. J`#O,CB9 r$ ȡae5HaIW3tp>5S)FC%^?@}^T4`nSYWUGMSs:i+%eU塈(p`RύhT7CNªKݧb F>-י:iJ%$G\rULYRHD XR@ym:[X0n% 2 }#ʹJQ?C15pn=O'ϕ?VtHW`P.aݮNgJ ]z"ά0t=j,\`9fpșW~y9ejV#oP$?qss݇aX8R38qm֪bm6Ҁn`SHa7#<K"wDWe~AGfdul^EL]x$q[Q S$XbV]FةI_2Um$-dDri cV0'r;'%WїO~;:_OL:)&@s.] Nە|gv~ϽL '9'+wK4Cπ [\r(*ZB#ޕ' XkrZGWP6jz[єFC%T>wo ꡝr0);F wc]Z0_]ؐ ;O2"4 z@xUƧl:C:H~s(!5/=Jw̓XՓ<4F+*ܱg]6}yN ?|`.JNc*ԗnl]ntkJ&Mic,ZF镓`DBDyW;ta.q̬LQ8r+hPYybRڛŌo7'.#5Ϣux?m8; g5%q(G_]j}q EҿN|S_ BZ)U7w-Qvbgzޝ{n(xz5 Zj[vDQ&"ͤ^ut(m|K=be-0FDRgǑlޱ(9,Юڐh!@PFp*'8ߦbn~1s=<Y ePзp}AZ?HUc4}nK0HOE |.}JIN ]l's?{in3IﮱI(&] WUU}|#[,$P1{%O8 X΋tj#Y.)t `gxJ".۬UBCR.~wQmφi7nڌ*ERWEŃlXw8؏ ǵ>]l3>DIv֒>) N\suTՇ!*w]Oy\y0W{u>\ ~ÛjTUhʯ40>^^Jnm# .O9CD%oIO_Xt_2^3|J/'j|Dqo@OA gtC#BEAΓ~Q1QGup_j*_bPWHN"0aؤSH.cjߗ;Brk ̎i3#ӾRR Nl ^t7O0>T ݫM&˼a]mnF:2iΙӉ S?u[9WCuԁߠ釲o\[^%du.~ P$?` evV2PuQSG߀@J%F}}j/'ZAaأhF+,VS"xԁRw wD;lMt` 3fИVJen'm BT}z0QQT=TP䰶Ƥ D9HNF̟d[߉|"${;d=##o:Mk`]+rTz.DN/ r DZv 7 x\xmjnŽłh0c ses2rQ ,̜ g FlHJiKNh?]f|+'__w{%d7uՐ=IZ=+\-4WZ(sh2st[ 1O*QDc^ܕ?Hy*ΔY\kať&"u>aLsҜ[n  6gͻ^\hG/JI+HH"0 cCt~7چoIы>Sՙ-[ ƽm+ƠRCZ,s-nU6Zㄷw@2Ca3ɼ]?`cY7.09qH`8'1ʡg4p|ʏ6ƞ‘N::øh#7*o1%%>` ' zídkcHvq{$_cĻr*.7ƭ9]w=`4EZ; rP]糼OHzy8cK7ݭ 5؊6es游Z czN)Vp{N6E4M<ɟa.{QNe|Q#!F *=DUF]ܤ2,ZstS_`ڐc AoX=?q Z9QKא9C?WA41 Z0 *0+߲c04u{z57Dj$ѵ`5KIS{~꧶Ǹ7sWr&E+`S=csٯ$o/g>,K_⊉[Ylz@*SN_ˮ/V66O=76ϐ=oũ- #~SA 7w#ӫo>wԝd%#Lݝ Ec hD~mV}~luh?Vbܕv_U#O7[N|f/I,&@˅Ogt>Фٵ؄&G0_\/*i=/zEuMZrU+bi(m}w;_IϋhDز@>gLZr,e뜞P:V,=s=އԍ[Cl{0U,c ߬esQh{!SJ(HSH)@Sk]*?JKk4O?>"Y[.(~b_*x}_1Sr^SL+D!D7& DԠU8NktU2uW_Gu jZl-J**}s,NbyGN{_(8fZņ8A;fU쏾55,TSDBŁf^l'BLv> ϼ\&U_n]* ,>ݳNT-DJ<$^KVW%9?3EPT#Ph. f;kZS~dw0re{Ԟ'n2M I߉Onv 9`n6H6RS*wG}ŠuVi% V8j#yzA]0fHGd}{|;:p U#] jMԝO]{*x~ڦ)g&04Vl˲,7tڝ%$Sz*'5ej}=IɁH暯,4^PYJhp:X xX5ċ {gˏ$)NnyE*J+gqpq ODr%;?j1xPRwJ3||l^uT'qVIaV$主Ҕj`IR.\76"طz%lQڛj]0+*+X?[NQ/;Ħȅ%΀}~QmJ~u`π- WD?Y-g.lj{8C0lpƯ 2K)oYg j¸J49ȟ6S0 -Hii㧵0T^Eo#eOb~ Gmc;RQKG||CqH8R׸XoU;=aݙu%eqeHv*ZiQ 0Mb^A*]&)|E3&觮( CMiKꍨt J摇悂ʝ `]8G2Z>qc 4+g,Tޤr}ĆZ7-Aig0(CMQ锅qGlg{CC,}+Qg_|!?۞4-.rl.+d f,_6ōL!DJ9=TzDa?>z ,^DT3,9X9ގ+xHN]E_r'NC=5nzF5*N)D`'}9U|x(Y=׶KTUCA+WAQݗ%6ɺ6[EvFשPq x-M)-RXG堀]ĴcN PO" r6C|X琲(='c7d={!~_,)j憢jcBx?)ۅk4̾hM`lG񊤼\ֺNPGbT./~eO nQUluu)١JR0J' =wCY'%[2脤,=t)LDY-װ= q1KݱDʽB4PéTNE~Vo/^D\fOAR` 6AH*5%K&ĕotSC,[*#/'kP͙{Rj+j- ƹ vɝCجb~E$:kEBly',.LSL)B`Ytt)At"!f=ކ\Jc'޶?Xϥ0j0?OE\!'ΘךMWAzy%9a,tUMwNDO|Ud5=. =dr-\TJ6;}r-m+e$R8V|. ߽Q("qSv*C\;z"KU6_դW [{>:`[e l޿'uy kq4*' j"%JO10]ߥWҨ\Av5W~ N|â@&wDID"!#/voNiU>KlNiDy3+ )@C_vz4Bh+h3O_!p ,l>@Zv J/iӛ`v|%,i7[F^|u6F5LS-_7-C˛dqD.&Gw,+#1p:O-zc*Q"K<{HM2G8bޗOdV |ì(%N[wx{#oqxn.mlT< 7!9 ߧ)F)"t`^(MoΣ^-S=ˀ4//3#v=X c# yÒAxXW}rC_U~'[i%ag/u_[o1 O'}'@If!#?#v6ڗ#-w/d^HJ4{)c:O}%i#P8'囄@1gԗMinJQg !~Ύ?qRfĵD0Ŋ^܈ jxZ}jLh̓} ҽʖIc)q-PA9*uYVK#+#{͜3V} f$'c?˓"FK$'v} [ yz wWT 7 d`?Ve`+dfFn"q8ob^on-EWˮmr$_wA3jV["2wB]W'C/Yͥ\43_Έ:?afJ`|N}P7lx(t/aǤR[7]>$IG!fO> t2TR.6(Do7'h̍iQ˿vUN7)6'BR=amng=C<P5i>.4lyq5{W 7X΢ۭF`i08b㼶Վ ̘PY4Ch$.n.ѓ+T 2 |NJe3Gwڥ9OjzH+!hO_l܄ڟg/taHzq%!]CDkP^\ȿd"3X*sOc +zخ,᥹^bY:w}Ll1xWI !xXY\Rˡmq]mǟh ҴV1#+|8$܈XΆH_/mW>zƈgǂ'!I h2H7)K9?0=, lܠ/t}1m2v8Gnm 6,{omRΊТER/UY_BN5EiMhUD`Yj&VwZp뚨~㯉|Ү۬m\fw\&CxT$FXizŗRD%*, 7%2w]ϔmD)3uqKLyX)n|R.o CV%'ϵhCT?rXTnSIv2pOPMp:)ani:(r|Mr KBA֦!2Ț}N*ȑcq4KG;o; dxqh_x0>$=IZ%X_zz{"ol[e\{3C>v_}(*Oܯn]q~+@ܽiݛӍMƚe#J˭Fˆu uچG3] MTg 6؝N9Uz~>VbOۑWfFΒe?}RMdFW;Y6ѺiaK(wo1~,uvV̂lY-(\VTO&`O^VM$sLesxcMs-\i5UkSS<~sh,vfWCݡ#&ƞ)T.^`|=Y1i pl< TuXDr,RO9~hnEg-Rf;Z U,yAKfG Sxi4ųg1콡pڬI#@>-ɢ>!,$"fR`ӚWRUt.̒~QT) M^ÀaAK=B%mSj%t(xcn(0R1eRIC Dc׫$=8>꘶{j'ֵ֫AJ,ڻC wZjT^Եm1#f96ofjd~cS=]t:b<'yԫدgE,Ow(|f/YsPq2x_pD|y3-,p#)r@fCmT16,W>f]Q1~/=Hٲv?_ZBE{nΒ!d`Kb#t(n?|=dy^|fwu͵3RJȃFݠ+ȶF Fgx\vFchčWgAڷܳ1AF @l!r4V])Ⱥ6RJuVߘJPrrVw~Z}>YYrX+~n6CF d 0 R&_x;(x" Ȑ%ա_AWip?=0bKQ;e5O]~=M~4˼Eضc{9C Nhh͝`C #EaK/cߟ{fjdRˆKh:omH"ƶFf0Pu.c4Xg+!mb]1K1z8~޿C^p"i Gէy*lB:ryRf*zK*7ZgjfߞlCS▚O&qYWk*i,3 H۸-ol~`X qV0-8N?,uYuD$,T%xgjv0hU[y[n`Ìލ󯪳&6}q:01؁|y :-NH endstream endobj 749 0 obj << /Length1 1630 /Length2 18141 /Length3 0 /Length 18983 /Filter /FlateDecode >> 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_?,fP 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!;@\JY=Ha3 lp# +< o`(0@"o¯”&:ʬso#Rxb0qɦZ\2(tRN?Ze60&k nX"|V FU$)v9`M;ݛپQpۄp.;]nߺapl\tKQBܗ} >%zGE^\U8&9%O1fNO?۳dE280WFNዂCjx}<:.8;ė[nڮ8xC"%٠j@z'xz%8LҌо> a!K|,f:=٣9=TK@clʼ[x1">DTI^do,Ë2ECs@B-m-]<Jy.tctά kSXHg$؅ jwFJ8ugpv R%kn?3xqU]r{fj5)` eO)^Gb5Qo%PzvZ vS>>ql`9k{܉r՘ JڢCxNd[{d0O gz'5H܃㧔 t=!|/5CDuM4ڎbeiLDFj;6e{)ƃdÊ`hWĠY*1&) 7䫋) ݴr/Iv/kt.Z4v_n rrK܂=` *"P|7yޚIg>֌M%~zC<"H&J̢Ӷk_?-_ t&EZaڞkZЪU Q:᱇+NI҇e#?{J]ad+Xzh8> VzY TlSI#jU~1yhfXп\$CiݺKvM'=@(BoUL5jw<>FSˑǬb[Fr'ز099PU n|p1;HF Gc=(e'ulZҝQD>Pe$nAƘJ.>>OBSP5w074i0xAA^oU@#{XVxFQ}*'xm`[9M,px3w+lءlyF@G)T G>?1=NN]B!DB?$q9;:QoOo}H՝;;ziaCe*?["\SV\]+a7lf.}8£ ґZbƐ"Ua8A҅@sue=rťu%쑌oO8V[66`Tۆ#)Utz mNG|(&wRs5:ʮFn !PP>=jrVhA{/pЛ endstream endobj 751 0 obj << /Length1 1647 /Length2 12104 /Length3 0 /Length 12960 /Filter /FlateDecode >> stream xڭweTܒ- ܽwwww Ӎ4!ww 4;ݹsgϼѽԮUuvZ>3MR`dmoW*0-]d!&v7J h$L @>6 4Xyyy`'kK+FSM_\D"-AOo?\v`{ F?T+ WVѕUH+i [*.vfk3 H ;qA% 08;ͬ€f@ 7`d ٹUwAN77L q6sv޲HHN o0li6s77bb r@rv&o.d N@K's;_>{;{g g+[N3[nKk_" XYa7wq' thڷ"L ;9Y yK '+'k_{wj);;%Ǟ-m-;'_ 5[Y);\bf0{ s5&o-/58AÛnw"AlۿUަG&mEYxl,o9q?M 𙅉ϿNF# 25G/M[<@w"،?&5# R34!񹯇f(ġA0߿[a\8pgGnw׎;xGCAۛ~7ٰ%H;l^aVEkׄa34uz9NWdo)fcYK\mf~U d[1o~@&fcj{@qG7p~{+kL"cSaz<;U[0 ֭W,G_z>[qNawaaVB{_05~Η0_/^ zw-@'z{%漺"@?KvZ?=.~az(y+W:lR_zՉVzgMMQ)j-Cͥ9#ߵjv'k!TΟE`+c&A!7kRZwUc{T(2rYI53$;oo\=?C la:;DgC|$Hr1}3|xXp FӨHY(6\K /3U~ wQ(}czX:޽"˿=laMC-:٤csc Txe7}uqE0:~9CW (wq S5ogh,<3n>yrYL(YG:?ȁ#k'Wy;gqkkH8Fa#4.eLh +Vx~]q=ʉF*bW|x>:-勽RhQ8dѡX`5{ϡEK`hFVjv$g&&VOFҊ8/.~ 1$Qơ-9;ζafP[Ճ3dWZμ(!n:_0|1fFZMoEƸ_%i?#<G˦8H0"=Cb -YPj"2w5%F'"X|6FR'[a'w?d e%2 .2QM2 _ew] *iJ/_;+LmhzC_<5bb~ɵ 2[a"a>$T򙼻 Jfa( ޞlΪ|zG\1QDTU%<W}!d4^{rG7m r(hڎTts& 30N#>dlN4o0XV* (u>!:GtEna}o$5(~QbWH(k>Wm$8,EjP1An?x8f#\s .R6Vܵs9kVDřǤ|R7xƐuHΓp04<#M:W {Ӵt}FrH eی07}uT1N6h׈35MőftELZ&d5i Hս/mp0/|%ԑsy>Y:놠_Y7ܖڻ-iY˓P=ᧃYbX۳u:4N9\TwO(*m3g/@4-zʹ~\.c$F# eD`BvE#XFvj&u/Lu)#Qc$༽nKaRwG-mx0_s|Zcⵥ0TF6ӭ[%z^\E~@ևv7j N1ü.'^z}zh^*k9e⾂G27%HvI!޻௖g<7pKdfKGXU Z10,ᗚ('snaE3:TFC 26=X>F3G -w^h?* s 9nfww<'z&0MHԀ|!riB5^'^%mg&4ƀv߶Y}ZӖl rfp"k|]BF=^kKE"gz6xi2a/s>֗-TQR 2QRGaڅӤ+{ M%K Z]V 6 = c?]c^]_#d`!vP$dԦ}@}!񥚌qsdEK/wJҗINrH936{(`,<%Cq#%H$׵&,+AסbY~Lw[F5^O\1O A[_ j$+YjWdP+[DNT"0]{lj"wolү&ܲc<,| $=j|!jimQR/ktE% #@qC Ta]mnTWfOߡn 9}ְOvоFݿ5(&" $ߎ O7^YN9#\æ,*ٰG+7$!@͐aPue5#w8^ޅnL5x)t6$Dy#pX b>4E%#o/4}SmJtVȦ!ͯU[;9yF9 YF.I~֖3x-@pv#_O{2{ӄ@B"X}dYB Km:8w;"~JAwŤ YZFn"*3!3h0v<Ssʹ=@P懇TP>O"{\3J.g.*0674T<WDdd^2E|tY~"P i<&X~A>ʵH?[P<"pyxTb%ʭEQf;sIk1CaU*}b eV5I].$(]b `#y+44aUGI臮3^9mXϺLT?UsbZ]6: p'(WWq=*h`3D5F& zSL,]Ap1475Y{ۓ<:dBK#,H͝čBhE2 UwF䞵P|g0fuoܳI.I +J:R[E]2h8ƾ;:c4_/mmҿ*\s 9񛾾RYqr"gcz)t9[lo1[8*ciԮ5͋)&{$1e^pJ%O-!?k>Nx-00MLh(+(i?;ֶ_J7ҎGSBq%`sv,*4QZ1~_nR.FVVjvZĥ>!: EZw XuBuzYG&P,Wp_He&H{ϖJj-Ga_qϕA8hitѳ~r,_Q#s[+s#Lq$l-c8_e:xNf/b(Ѷv0zi)+~%߷d;ʮ CJ6|\$/8tMp '֣e7mFs3X|#rۚ D꘻BZXj-p ZsZ/Q׋b~7<0_W{wNSZC׆S||KkΔj2n8Nft|PA͈^3xczz#iJD*)sV6ُy:vR2<ӽ}gFe0%“{+Hy[\D(qj$Zw[9f FJ $Np\ܟ[CqbFme7Mr~)7YazD5?*?+2%HUi1\t a \M+*A<,ZPj(S 4Hţ\JpW)9FmV{9Xګ8,}zȓ)Pڔx@ \J||9Ho[uo^]1H1̬~M__'$;G{xckͺoۯ'bӌ@0i%,6g7>Α/R3C'#vRϐ2qfLBA\J!-, bAP†*K_&)}0e12D\ ۥ܋&rv Cmո?˗ ²qSHYWOTlpDXh Ӯ\jEe\_<43A4Ể=&>,k$ÚNb>7lO}@7M/ P `;r Ӈ5Fcjug%O]j%G%,(-XtKJNUMpov)cf9e:nohĕ|ԘC$: 9DkR\ 7epP48cjN>GJOB=PՕG, &izN^ːAb"Hf2p, 't"_>\S(/xBQXw\@a1do9҇٥z]cL;c`q;rٶELE}~oW&?^Is(?m*):rXR3!Oguti he+ cyf}l&ՍUo:AfǙ{u7 Ч\[Ps(gJihIĉS𗼙HMJ$bwϲyp{=Z$UpHs'Y3Fj$WltI˦V=|= "8d\Bnbx ݒ6C8e)'ѐ*DsWo\nɰױ} O-n(+}쨱(;л>Fj)UMHZ.~`<ؗ**'YaqMR ׊UڗS|҄+xM4FDCT>[\<#6:&fg,Bn2gPHB]! Fxi eJث{f#x$ġk=&-3NHYDK/.\bdm2/tJy0'5&p M2>MZ7ʇ9*A `dzWif 5Ai>ʚFm$FQD|5lP)R#Ekү)5gxS-)G lk`xw'~38LBq*/9bR][)ywwFs8ҟf.a|27D|pz^̯”1mgb`lOvuS/02r]E13LBHqYɐYPJ(/DycW|-w@ZkR9K}W$ QYom%V 'MsB}9N< ؛'"j?u։0jgrAݿ(V3z85rZ^ (;-2-9@M2:_:'젵AI]dNrH6";w1<@wPItg_7㐃]gpw-?(gqȒ1`^p`\0H& <&P{;ev 6D:{*/B˫";&4΢&(N }:?5%"QLsnyf+˘h'#wQGp+yuȞ|4}W?=\>O%JD=L"T?äT]/)m MlVm[Y|#+3atۧ[ 9TZ +u jVz7w刑((i#BzewKſhIM* iZ֍8})FρȉZ]"G{1vVsͷVn5nPܕk5oY+i,ѳOYݔL+, ͿڐزTBoU;Nhtz*cB£]C7/2+2&;^V[~_DOsCag!wmww[%Mًk#~6.|/[qD!~^쐗dTT[}Mi[;}n`[Nw4#zf0grsZ"4|tv; ex ';>[%QN1|Ї@Q"d2:@1,ቬIFr+-HI4ːo䶋B3ΈիVҡGY-IЄ`mFzJ ZJx`l/> "=ig.7\~ 60U0b)27qHk6ju35~4b6&+pI$ͩ{~;Y^y;&~hokUIv_Pޒhee9NKv'k400 |;+$XyY4f(}W)^ s\\ɛle[9 񔓑pfd-7'RuF*uGn⎖-né.8uo8@Ya<k}٨n>Ӣg>t)`ݓd{J}Ĭ v+9:XRCm8O9,t#e(0sCV#tCf~N]$ݮ1& s6ڗHa$.^MȖo9*Q$<"[%Re0/rS2UܘpeuD˯]}`IPb*]6wA0^_PfhAy%Hq]LIBk/+ݜQ|DRU;qyD P-E_JkK;7Za}렂 !OX'XY~Wc_7 &_ئ"p[ /!cw-qJ .|YƚQӹuY}W W~h]&iaydhub EgHhnBb?cЗ̮(k = ~E9e$^ǩ[[j=°(߲zQ-,VH5b@";ݴ, w D$!3^VhEmΤMfՉzշ-4U3f+p%=1B7*p|4P5gZ.I5J i :[^U4u2o~RDM>2UZ#hJyw%.UT";>T ( :x.V9:s4Q`)q,#{7i|$XVVgJ{bZSm-׳$ϱ-`H³c2"*mL];qL7[ 7@8 ꘣nDz`d [nе*Ex|C\֪h>bQm^KT esKj [+X^*2L΃d85|p̛YHE#@XѻNo6?En_~)%w$­۾ۊL)@,`i\*x%Krc{\gktF`i`JMF龾 tM"LJ omWd7/D::^t2hs.Qty6הN[TAalf"EOO xAhJa-̱DmLQ|ɢ׊SI6&B?Y| Sf_zo=y;F7a@s*gGSymNMzi|$ "aPtעXQ{ڽbqìTKYw:GeO x&~oxOK7n^mH&חP)[)VyZڊfAaJGl hAsEN, ܡ>[D6? endstream endobj 685 0 obj << /Type /ObjStm /N 100 /First 913 /Length 4339 /Filter /FlateDecode >> stream x\[s6ھ3f:;㜚v]\6k#[dyAt {x7Q(MdV J&Z8P'VHڣA%}mDOڢ]Z>q$(hⴠA) K d%0| aMи:6 6S1C! ig5F Ka.1,żG |+o cYQx'x1M ӒD"mhJ D"p@rm:"p(Sr  ,RŢwP\H]pJm jck 06Zwj'#hِ@#ĉȴX3Y=6 1 C6R?(`OWWy|szO>ȇyx`|&R~8k8S)Ɖrƻ*<ݡ;!~z.cOue_~F B늭%Sʳ)[iw`4P16[lt+ *Œ]*ApAU]P΀2̘4OU+hRAЈ4Xȁ4`dX']TE7qTZtc "T ƩX@9^8plm1pq{R'6?q, 3@EDB hxcq~_D$ DC"0.5D+AE>6,'4a! \qnW4 m40.*X`*Vj9Y0 Cbfy z<"xPo$Jȩ,Y(J(pzL71Q#D. DdPIcJUH-1 K  Dpp A!!DJLy6!g3EMTѐF5OoUbz|r\kAC!ψ5 `(5\WB0K튯y FMafUŁ BICHc"Fi:kVD\ | I95" veW4Xd݁1!s+@<JN xP 3v,Zγp(XnO@g]PE g y%%Z;0GK8f[0"FD2 WaZU aHc`a#Z0vUEq`waTUXыt0o/45^:s=oED@MqTԱ)Lo`][WBU^/^fv){:>5uS^>ަp}@B~@h1?;G r-ޭlG^9Ke9Y˼읫^*-t ]RBr9C~LDly)Lmsr~`{26nbl[1b)$(">#ѭD8SHC 0 Ax^"MD6|$l[vKzOrSH,| cN z1Tsũe_>, Wc*BxKϫf?[,{^uhH IPQړ)(g-8|< sJOLDxINh i[9F筜ftƐwU &hNAjLvo&Pؗ:x炗ĐQF>/V!Fo,[dZN _H棙JzMs0ӣk}Wy7qÇEݟ}?%G5/c#b|/0W'6j_~O'{1N kerp Wqs3}Cpm[txyTE.YnRice7竭5}қ5NY߶TZdʾ"o4/EjEo"Zl%}әOaVYBv}fS>Zd~}^[^[2k;k K=M-HmtT0 DW\RT)3ܸ)lu 3Fm]Rm{``며Vbb,rbvs~-W֮V}\VxQn1:j) fԝsݨqCteP>}QmMp(ogKɌ;훯q6Ro},b1M5 @iawo~u-] jۮN/ʹ4llļ{LPuṊ7mj#X. `8Ϲd%6ՌL\ń`;ɶ1?F&ڮOmmV]/m_'Έz"6"jZVV޻t\X*]յQuiav ,ޕvW?9ټ:wi\ań~> v3}%7޿ZDo\z G$^-foɳW{ɇM| 4# ^-Zӳz { y~דìF *_DR|e ɂW|"'~׆S|οB0ka:<91:~-dd瞉Vlocaf's<˛^,_̯?x8o5!;w^>w>wگsZHQgP>cD>|)G<FN<]L>׬>EsY'B^ȋ?>]Wr*-?ʙ˥Wr.?Ob:? dy!卼:˳/uqY_ܯ7M6#ۭ;mq۾Y)CQc%O'rru.'KvȊNYuQɀ@o 2,7&oPfu;^O?稀\\&(5;g O,?)|3OgrʒnpS_NFob9abQ&~op%S/ O>~r\?ΦX@_ @ f`_ëv"w1{QbbRkWqQdES{$!+^Һ)U.A}ጾq(I_@Ul;E[k-löΧFl >{pS,]ZG;ZYS *YٽWNpӫ=;;" ViUZg\;m#D3'G>)̿_d}gɯ9?Wi3YO_ agyQW엶ֆ7F# __xǒױ=ɇ&yGkw@ 7x.AE7Y} hms)m$0{/[&c>:-P>Wh>{/s] QQIfEuyhcMnK{ru6?G2 CAW:i&*r/kƴ0K Htxpis`ކ'#%#q ,=?/㰱 -Pi&j0')"4e5MXz))IaVu]Nzyn;cjz'Z8r^Y]>0 endstream endobj 753 0 obj << /Type /ObjStm /N 100 /First 871 /Length 2767 /Filter /FlateDecode >> stream xڭY]o9}Ɍ *-F $%N`5J2{v7C+wWO:۩:iL4 ?9;3tLlw>@_y q Vm*BG<2y`(1Bpg<y,c@:.1eV`EDt:Htg!`a<'@`vZ`Iix(;X0hɳliزʁ%9([ ,S QZƒ͝j![*(; $1y2].D*G^au.`1+#yXc; <"+/s#Rš8kGr ( XA>HPƨ1iKƸU :oNӘ@{yb O`bf2b{eeXAtd003L VBpH9fPjY1r<|yë)ʐic2hv}r \ˏlFq;#{,L8S2ʐ] {ًw7StN-FDg lD$MPPۧϏj`϶&6qnn|BȀ m'ϟ|${u*?Yh%[X &a wɣ'6ov lgtѱc#a8ܿy&Fϝta|X}{vHvTdۥ!'qK(އ 6iLi}6 1>.4wHi:PqnxDZ5C8Aķz|z>B$$1>H4[܅nL>jqVFH|:밹_lB&LĤ)ycJ݀B0QDrB)=vHKդ߶72]bR*eK~ijG\2v;f^NytRemq,lg9iq--7K|6_ϋeȶtn˷#۴.V+ h{Fi{WpO-i-r Žwܻ{ލEmkqoG"޵#vammGb%Qe-7LK뾶O3FTQQMkfKuӊiV&.=A붭anAqPKq(~]awEOapG5FAQHo-VXD@5"``(ɏzNajVK3y b 6,0jco9'& ` ͵[(.j]/P\VG*-P\tV3{)-P\4V(-P\V ;kSxe\atuI`t EJ RpR<*5 õ;G{TP> ݣf >CI$B='nzl /HL_|f,Y$N$`H&{k!3{F#,rF¬ vΙ2 ,.LΠa4`B'2 ),:+aY%bnNAX!rVȡ `^kÅAa `3 pqڸy6.'@!]8@<mxk=䊥*v"cKBO!=sľF6.zFBOxc'E pATEM\ߐstI!yNK*!F$>͹Yn. 2r 񣰮mm[Ԯk\OI&j䞙ĸ@Sh/<4R$_3pj0}7Px H%'q ՄL_8TUQ* 4~ׄ>}뜐뤲HZ_\zyJyDtI#ɞfB&\5DuH0\ 4X%͆BP9Mrgz$v}dL,HYgaSNXꃥkSm^/_B{8pN!tm& HD(ܼX.ƻ?sT{㙌 endstream endobj 794 0 obj << /Author(\376\377\0002\0000\0001\0009\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 package)/Producer(pdfTeX-1.40.18)/Keywords() /CreationDate (D:20190204155253+01'00') /ModDate (D:20190204155253+01'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) kpathsea version 6.2.3) >> endobj 787 0 obj << /Type /ObjStm /N 7 /First 53 /Length 309 /Filter /FlateDecode >> stream xuMk@=&,PiRvES?XgN1΃G!|g`A@@ΠF"9!-Õe y#y\.HHHJ T]YC6kޜ46zjG&Z=O$S%"|ePNî* endstream endobj 795 0 obj << /Type /XRef /Index [0 796] /Size 796 /W [1 3 1] /Root 793 0 R /Info 794 0 R /ID [<6C1FFEF548067EE73E44B93E4FC5BFF2> <6C1FFEF548067EE73E44B93E4FC5BFF2>] /Length 1936 /Filter /FlateDecode >> stream x%Mh7J,ٲ%[vlֱ˖d[lK:d_Phhj&%d($_B(@JiR ]ԅ&Вa̝;3{&BB%X1{F'($Bbypi@vA_H# ϓK@$1Ky#b|>f"6qt hV3[NAtԨ6-9Q}K l':0$M /ҾEX^dGֺ`*[mEl6V!nmV5tm6/vZFڏHi0.vqIVt>4Q p؊_>i&"C]&!Dڀ8I+:[@?/Z1#MyW8` dk@u@?4 '#z fd @{3>b|Ԃ [tQ?ឝ8p"4N" ss' ΪLvs ' 㹓Dsst\uy>bŹz6MVJZQ֬ ݷ=kK}IK bsub < submit.job mpirun ready 1 1 1 $nodes * $taskspernode $threadspertask normal $jube_wp_envstr ALL job.out job.err 00:30:00 -x ${submit_script}.in --> JUBE-2.2.2/platform/moab/0000775000175000017500000000000013426052212014671 5ustar sebisebi00000000000000JUBE-2.2.2/platform/moab/submit.job.in0000664000175000017500000000071413426051426017305 0ustar sebisebi00000000000000#!/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# #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then exit $JUBE_ERR_CODE fi #FLAG# JUBE-2.2.2/platform/moab/chainJobs.sh0000775000175000017500000000052313426051426017136 0ustar sebisebi00000000000000#!/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 JUBE-2.2.2/platform/moab/platform.xml0000664000175000017500000000707513426051426017256 0ustar sebisebi00000000000000 msub submit.job mpiexec -np $tasks --exports=$jube_wp_envlist ready 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 JUBE-2.2.2/platform/bluegene/0000775000175000017500000000000013426052212015541 5ustar sebisebi00000000000000JUBE-2.2.2/platform/bluegene/submit.job.in0000664000175000017500000000113313426051426020151 0ustar sebisebi00000000000000# @ shell = /bin/bash # @ job_name = #BENCHNAME# # @ output = #STDOUTLOGFILE# # @ error = #STDERRLOGFILE# # @ notification = #NOTIFY_MODE# # @ notify_user = #NOTIFY_EMAIL# # @ wall_clock_limit = #TIME_LIMIT# # @ job_type = bluegene # @ class = #JOB_CLASS# # @ bg_size = #BGSIZE# # @ bg_connectivity = #BGCONNECTIVITY# # @ queue #ENV# #PREPROCESS# #MEASUREMENT# #STARTER# #ARGS_STARTER# : #EXECUTABLE# #ARGS_EXECUTABLE# #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then exit $JUBE_ERR_CODE fi #FLAG# JUBE-2.2.2/platform/bluegene/chainJobs.py0000775000175000017500000001337213426051426020032 0ustar sebisebi00000000000000#!/usr/bin/env python from __future__ import print_function import sys import os import re import datetime maxSpaceDim = 15 # max No. of steps in one JUQUEEN job defaultmaxWallclock=datetime.datetime.strptime("06:00:00","%H:%M:%S") # max Wallclock time for 1 step smallmaxWallclock=datetime.datetime.strptime("00:30:00","%H:%M:%S") # max Wallclock time for 1 step (node <=64) class Job: globalJobSpec = [] def __init__(self,jubeStep,pwd): submitFile=os.path.join("..",jubeStep,"work","submit.job") try: f=open(submitFile,"r") except IOError: print('ERROR: unable to open submit script "{0}"'.format(submitFile), file=sys.stderr) raise self.name=jubeStep self.pwd=pwd self._parseJobscript(f) f.close() def _parseJobscript(self,fptr): globalJobSpec=[] specificJobSpec=[] jobCommands=[] stdoutfile="stdout" errorfile="stderr" walltime="00:01:00" for line in fptr: if re.findall(r"^#\s?@\s*(shell|job_name|notification|class|notify_user)",line): globalJobSpec.append(line) elif re.findall(r"^#\s?@\s*output\s*",line): stdoutfile=re.findall(r"^#\s?@\s*output\s*=\s*(\S+)\s*$",line)[0] elif re.findall(r"^#\s?@\s*error\s*",line): errorfile=re.findall(r"^#\s?@\s*error\s*=\s*(\S+)\s*$",line)[0] elif re.findall(r"^#\s?@\s*wall_clock_limit\s*",line): walltime=re.findall(r"^#\s?@\s*wall_clock_limit\s*=\s*(\S+)\s*$",line)[0] elif re.findall(r"^#+\s?@\s*",line): specificJobSpec.append(line) elif re.findall(r"^[ #\n\t\r\f]*$",line): continue else: jobCommands.append(line) self.__class__.globalJobSpec=globalJobSpec self.specificJobSpec=specificJobSpec self.jobCommands=jobCommands self.stdout=stdoutfile self.stderr=errorfile self.walltime=datetime.datetime.strptime(walltime,"%H:%M:%S") return def writeJobHead(): return [ "#@ output = $(job_name).$(step_name).$(jobid)\n", "#@ error = $(output)\n", "#@ environment = COPY_ALL\n" ] def initJobStep(step): return [ "#=== Step {0:d} directives ===\n".format(step), "#@ step_name = step_{0:d}\n".format(step), "#@ dependency = (step_{0:d} >= 0)\n".format(step-1) if step > 0 else "" ] def writeJobCommands(job,cwd): stdout = os.path.join(job.pwd,job.stdout) stderr = os.path.join(job.pwd,job.stderr) output = [ '(cd {0}\n'.format(job.pwd) ] output += job.jobCommands output += [ 'cd {0}\n'.format(cwd) ] output += [ ') > {0} 2> {1}\n'.format(stdout,stderr) ] return output ################################ # MAIN ################################ filename = sys.argv[1] f=open(filename,"r") SystemParameterSpace={} for line in f: lineArray=line.split(':') jubeStep = lineArray[0] jubeStepPWD = lineArray[3] SystemParameter=(lineArray[1],lineArray[2]) # nodes,topology if SystemParameter in SystemParameterSpace: SystemParameterSpace[SystemParameter].append(Job(jubeStep,jubeStepPWD)) else: SystemParameterSpace[SystemParameter]=[Job(jubeStep,jubeStepPWD)] # Abort if it is not possible to create the chained jobs if len(SystemParameterSpace) > maxSpaceDim: print('ERROR: system specific parameter space to big', file=sys.stderr) print(' allowed dimension: {0:3d}'.format(maxSpaceDim), file=sys.stderr) print(' used dimension: {0:3d}'.format(len(SystemParameterSpace)), file=sys.stderr) for (index,parameter) in enumerate(SystemParameterSpace.keys()): print(' {0:d}. {1}'.format(index+1,str(parameter)), file=sys.stderr) exit(1) f.close() zeroTime=datetime.datetime.strptime("00:00:00","%H:%M:%S") commandlines=["case $LOADL_STEP_NAME in\n"] cwd = os.environ['PWD'] step = 0 fptr = open("submit.job","w") fptr.writelines(Job.globalJobSpec) fptr.writelines(writeJobHead()) for SystemParameter in SystemParameterSpace.keys(): substep=0 maxWallclock = defaultmaxWallclock if int(SystemParameter[0])>64 else smallmaxWallclock stepWalltime=datetime.timedelta(0) # init with 0 commandlines += ['step_{0:d}) echo "Working on $LOADL_STEP_NAME"\n'.format(step)] for job in SystemParameterSpace[SystemParameter]: if job.walltime > maxWallclock: print('ERROR: wall_clock_limit "{0}" exceeded by one job specification - ABORTED'.format(job.walltime), file=sys.stderr) exit(2) elif job.walltime + stepWalltime > maxWallclock: fptr.writelines(initJobStep(step)) fptr.writelines(job.specificJobSpec) fptr.write("#@ wall_clock_limit = {0}\n\n".format((zeroTime + stepWalltime).strftime("%H:%M:%S"))) stepWalltime = job.walltime-zeroTime substep = 0 step += 1 commandlines += [';;\n\n','step_{0:d}) echo "Working on $LOADL_STEP_NAME"\n'.format(step)] else: substep += 1 stepWalltime += job.walltime-zeroTime commandlines += writeJobCommands(job,cwd) commandlines += [';;\n\n'] fptr.writelines(initJobStep(step)) fptr.writelines(job.specificJobSpec) fptr.write("#@ wall_clock_limit = {0}\n\n".format((zeroTime + stepWalltime).strftime("%H:%M:%S"))) step += 1 commandlines += ["esac\n"] fptr.writelines(commandlines) fptr.close() if step-1 > maxSpaceDim: print('ERROR: cumulated wall_clock_limit to big', file=sys.stderr) print(' => exceeded max step limitation: {0:3d}'.format(maxSpaceDim), file=sys.stderr) exit(3) exit(0) JUBE-2.2.2/platform/bluegene/prepareJobs.sh.in0000775000175000017500000000044213426051426020767 0ustar sebisebi00000000000000#!/usr/bin/env bash if [ $# -lt 2 ] then echo "$0: ERROR (MISSING ARGUMENTS)" exit 1 fi JOBINFOFILE=$1 shift SUBMITSCRIPT=$* IFS='/' read -a ARR <<< "$PWD" LENGTH=${#ARR[@]} JUBE_WP_STEP="${ARR[$LENGTH-2]}" echo "${JUBE_WP_STEP}:#BGSIZE#:#BGCONNECTIVITY#:$PWD:" >> $JOBINFOFILE JUBE-2.2.2/platform/bluegene/platform.xml0000664000175000017500000001025313426051426020116 0ustar sebisebi00000000000000 llsubmit submit.job ready runjob "$jube_wp_envlist".replace(","," ") --ranks-per-node $taskspernode --np $tasks --exp-env $export_list shared jobid ${shared_folder}/${shared_file} ./prepareJobs.sh true ./chainJobs.py $shared_file; $submit submit.job 32 1 1 $nodes * $taskspernode $threadspertask Mesh $jube_wp_envstr never job.out job.err 00:30:00 ${submit_script}.in chainJobs.py ${chainjob_script}.in JUBE-2.2.2/platform/slurm/0000775000175000017500000000000013426052212015115 5ustar sebisebi00000000000000JUBE-2.2.2/platform/slurm/submit.job.in0000664000175000017500000000116413426051426017531 0ustar sebisebi00000000000000#!/bin/bash -x #SBATCH --job-name=#BENCHNAME# #SBATCH --mail-user=#NOTIFY_EMAIL# #SBATCH --mail-type=#NOTIFICATION_TYPE# #SBATCH --nodes=#NODES# #SBATCH --ntasks=#TASKS# #SBATCH --ntasks-per-node=#NCPUS# #SBATCH --cpus-per-task=#NTHREADS# #SBATCH --time=#TIME_LIMIT# #SBATCH --output=#STDOUTLOGFILE# #SBATCH --error=#STDERRLOGFILE# #SBATCH --partition=#QUEUE# #SBATCH --gres=#GRES# #SBATCH --account=#ACCOUNT# #ADDITIONAL_JOB_CONFIG# #ENV# #PREPROCESS# #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then exit $JUBE_ERR_CODE fi #FLAG# JUBE-2.2.2/platform/slurm/chainJobs.sh0000775000175000017500000000113213426051426017357 0ustar sebisebi00000000000000#!/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 JUBE-2.2.2/platform/slurm/platform.xml0000664000175000017500000000756113426051426017502 0ustar sebisebi00000000000000 sbatch submit.job ready srun shared ${shared_folder}/jobid ./chainJobs.sh false 1 1 1 $nodes * $taskspernode $threadspertask batch $jube_wp_envstr ALL job.out job.err 00:30:00 ${submit_script}.in $chainjob_script JUBE-2.2.2/platform/pbs/0000775000175000017500000000000013426052212014537 5ustar sebisebi00000000000000JUBE-2.2.2/platform/pbs/submit.job.in0000664000175000017500000000070313426051426017151 0ustar sebisebi00000000000000#!/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# #MEASUREMENT# #STARTER# #ARGS_STARTER# #EXECUTABLE# #ARGS_EXECUTABLE# cd ${PBS_O_WORKDIR} #POSTPROCESS# JUBE_ERR_CODE=$? if [ $JUBE_ERR_CODE -ne 0 ]; then exit $JUBE_ERR_CODE fi #FLAG# JUBE-2.2.2/platform/pbs/chainJobs.sh0000775000175000017500000000052313426051426017004 0ustar sebisebi00000000000000#!/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 JUBE-2.2.2/platform/pbs/platform.xml0000664000175000017500000000670213426051426017120 0ustar sebisebi00000000000000 qsub submit.job mpiexec -np $tasks --exports=$jube_wp_envlist ready 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 JUBE-2.2.2/README.md0000664000175000017500000000425513426051426013422 0ustar sebisebi00000000000000JUBE Benchmarking Environment Copyright (C) 2008-2019 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 2.7 or Python 3.2 (or a higher version) to run the program. You also can use Python 2.6 to run JUBE. In this case you had to add the argparse-module (https://pypi.python.org/pypi/argparse) to your Python module library on your own. # 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 ``` 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. # 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) JUBE-2.2.2/contrib/0000775000175000017500000000000013426052212013567 5ustar sebisebi00000000000000JUBE-2.2.2/contrib/schema/0000775000175000017500000000000013426052212015027 5ustar sebisebi00000000000000JUBE-2.2.2/contrib/schema/jube.rnc0000664000175000017500000001552013426051426016471 0ustar sebisebi00000000000000# 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, 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 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 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 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 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 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" }?, 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)* JUBE-2.2.2/contrib/schema/jube.dtd0000664000175000017500000001611213426051426016460 0ustar sebisebi00000000000000 JUBE-2.2.2/contrib/schema/jube.xsd0000664000175000017500000003755513426051426016521 0ustar sebisebi00000000000000 JUBE-2.2.2/JUBE.egg-info/0000775000175000017500000000000013426052212014346 5ustar sebisebi00000000000000JUBE-2.2.2/JUBE.egg-info/dependency_links.txt0000664000175000017500000000000113426052212020414 0ustar sebisebi00000000000000 JUBE-2.2.2/JUBE.egg-info/SOURCES.txt0000664000175000017500000000371313426052212016236 0ustar sebisebi00000000000000LICENSE MANIFEST.in 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/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/dependencies/dependencies.xml examples/environment/environment.xml examples/files_and_sub/file.in examples/files_and_sub/files_and_sub.xml examples/hello_world/hello_world.xml examples/include/include_data.xml examples/include/main.xml examples/iterations/iterations.xml examples/jobsystem/job.run.in examples/jobsystem/jobsystem.xml examples/parameter_dependencies/include_file.xml examples/parameter_dependencies/parameter_dependencies.xml examples/parameter_update/parameter_update.xml examples/parameterspace/parameterspace.xml examples/result_creation/result_creation.xml examples/scripting_parameter/scripting_parameter.xml examples/scripting_pattern/scripting_pattern.xml examples/shared/shared.xml examples/statistic/statistic.xml examples/tagging/tagging.xml 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/keyvaluesresult.py jube2/result_types/syslog.py jube2/result_types/table.py jube2/util/__init__.py jube2/util/output.py jube2/util/util.py platform/bluegene/chainJobs.py platform/bluegene/platform.xml platform/bluegene/prepareJobs.sh.in platform/bluegene/submit.job.in 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.inJUBE-2.2.2/JUBE.egg-info/top_level.txt0000664000175000017500000000000613426052212017074 0ustar sebisebi00000000000000jube2 JUBE-2.2.2/JUBE.egg-info/PKG-INFO0000664000175000017500000000360313426052212015445 0ustar sebisebi00000000000000Metadata-Version: 1.1 Name: JUBE Version: 2.2.2 Summary: JUBE Benchmarking Environment Home-page: www.fz-juelich.de/jube Author: Forschungszentrum Juelich GmbH Author-email: jube.jsc@fz-juelich.de License: GPLv3 Download-URL: www.fz-juelich.de/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 :: 2.6 Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Benchmark Classifier: Topic :: Software Development :: Testing JUBE-2.2.2/bin/0000775000175000017500000000000013426052212012677 5ustar sebisebi00000000000000JUBE-2.2.2/bin/jube0000775000175000017500000000200313426051426013553 0ustar sebisebi00000000000000#!/usr/bin/env python # JUBE Benchmarking Environment # Copyright (C) 2008-2016 # 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() JUBE-2.2.2/bin/jube-autorun0000775000175000017500000000624013426051426015255 0ustar sebisebi00000000000000#!/usr/bin/env bash # JUBE Benchmarking Environment # Copyright (C) 2008-2016 # 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 JUBE-2.2.2/jube2/0000775000175000017500000000000013426052212013136 5ustar sebisebi00000000000000JUBE-2.2.2/jube2/result_types/0000775000175000017500000000000013426052212015700 5ustar sebisebi00000000000000JUBE-2.2.2/jube2/result_types/syslog.py0000664000175000017500000001353113426051426017603 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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): """Table 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): """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 JUBE-2.2.2/jube2/result_types/keyvaluesresult.py0000664000175000017500000002256413426051426021540 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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("") 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 JUBE-2.2.2/jube2/result_types/table.py0000664000175000017500000001551313426051426017354 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 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, pretty=(self._style == "pretty"), 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): """Create result data""" result_data = KeyValuesResult.create_result_data(self) return Table.TableData(result_data, 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 JUBE-2.2.2/jube2/result_types/__init__.py0000664000175000017500000000145213426051426020021 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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""" JUBE-2.2.2/jube2/result.py0000664000175000017500000002240713426051426015041 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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.warning(("No data found for analyser \"{0}\" " "in benchmark run {1}. " "Run analyse step first please.") .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))) # Create copy of all wps to reduce this list while wps are sorted # into the chains all_wps_tmp = all_wps.copy() while (len(all_wps_tmp) > 0): next_id = all_wps_tmp.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 in all_wps: wp_chains[-1].append(wp.id) all_wps_tmp.discard(wp.id) # Add wp itself to the cahin wp_chains[-1].append(next_id) steps_in_list = set() # Add children to the chain, each step can only be added once) for wp in self._benchmark.workpackage_by_id(next_id).\ children_future: if wp.step.name not in steps_in_list: if wp.id in all_wps: steps_in_list.add(wp.step.name) wp_chains[-1].append(wp.id) all_wps_tmp.discard(wp.id) # Add all parent WPs which might be missing in the chain, by # using the last WP as a reference. This WPs does not provide any # analyse data but, their parameter will be available for i, chain in enumerate(wp_chains): for wp in self._benchmark.workpackage_by_id(chain[-1]).\ parent_history: if wp.id not in chain: wp_chains[i] = [wp.id] + chain # 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 si 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 ["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 JUBE-2.2.2/jube2/analyser.py0000664000175000017500000005336213426051426015345 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 + "_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: regex = re.compile(pattern.value, re.MULTILINE) 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 if "sum2" in match_dict[pattern.name]: match_dict[pattern.name]["sum2"] += match ** 2 else: match_dict[pattern.name]["sum2"] = match ** 2 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: 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))) 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": name = pattern_name else: 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() 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 JUBE-2.2.2/jube2/help.txt0000664000175000017500000007553713426051426014656 0ustar sebisebi00000000000000Glossary ******** 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.-pathes based on position of xml-file * "internal": rel.-pathes 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 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 is 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. * "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 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... include-path_tag Add some include pathes 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.-pathes based on position of xml-file * "internal": rel.-pathes 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 * 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" and "cycle" * 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 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 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 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, a combined result view of all available benchmarks in 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 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"; 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! JUBE-2.2.2/jube2/substitute.py0000664000175000017500000001274113426051426015736 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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__) JUBE-2.2.2/jube2/parameter.py0000664000175000017500000007275013426051426015511 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 xml.etree.ElementTree as ET import copy import jube2.util.util import jube2.conf import jube2.log import re 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=""): self._name = name 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) new_parameterset.add_parameterset(self) return new_parameterset @property def name(self): """Return name of the Parameterset""" return self._name @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 add_parameter(self, parameter): """Add a new parameter""" self._parameters[parameter.name] = parameter 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)]) @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""" # 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 parameterset[name].update_allowed( NEVER_MODE if (update_mode == USE_MODE) else update_mode)) and not self[name].is_equivalent(parameterset[name])): return False return True 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() intersection = set(self.all_parameter_names) & \ set(parameterset.all_parameter_names) for name in intersection: if (not (self[name].update_allowed(update_mode) or parameterset[name].update_allowed( STEP_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 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"(? $$$$ -> $$ # $$$ -> $$$ -> $ # $$$$ -> $$$$$$$$ -> $$$$ # $$$$$ -> $$$$$$$ -> $$$ value = re.sub(r"(\$\$)(?=(\$\$|[^$]))", "$$$$", value) parameter_dict = dict() if parametersets is not None: for parameterset in parametersets: for name, param in parameterset.\ constant_parameter_dict.items(): # Avoid evaluation of fixed parameter content if param.is_fixed and "$" in param.value: parameter_dict[name] = re.sub(r"\$", "$$", param.value) else: parameter_dict[name] = param.value value = jube2.util.util.substitution(value, parameter_dict) # Run parameter evaluation, if value is fully expanded and # Parameter is a script mode = self._mode pre_script_value = value # Script evaluation is allowed if: # all parameter were already replaced OR # last substitution before workpackage creation (force run) OR # last substitution after workpackage creation (final run) # AND no jube_wp_ parameter inside the value (otherwise force run will # execute these parameternames to early) # AND parameter must be a scripting parameter if ((not re.search(Parameter.parameter_regex, value)) or force_evaluation or final_sub) and \ (not any(parname.startswith("jube_wp_") for parname in self._depending_parameter)) and \ (self._mode in jube2.conf.ALLOWED_SCRIPTTYPES): try: # Run additional substitution to remove $$ before running # script evaluation to allow usage of environment variables if not final_sub: value = jube2.util.util.substitution(value, parameter_dict) # Run script evaluation LOGGER.debug("Evaluate parameter: {0}".format(self._name)) value = jube2.util.util.script_evaluation(value, self._mode) # Insert new $$ if needed if not final_sub and "$" in value: value = re.sub(r"\$", "$$", value) # Select new parameter mode mode = "text" except Exception as exception: # Ignore the forced evaluation if there was an error if force_evaluation: value = pre_script_value else: try: raise RuntimeError(("Cannot evaluate \"{0}\" for " + "parameter \"{1}\": {2}").format( value, self.name, str(exception))) except UnicodeDecodeError: raise RuntimeError(("Cannot evaluate \"{0}\" for " + "parameter \"{1}\"").format( value, self.name)) # Run evaluation helper functions if self._eval_helper is not None: value = self._eval_helper(value) changed = (value != self._value) or (mode != self._mode) if changed: param = Parameter.create_parameter(name=self._name, value=value, separator=self._separator, parameter_type=self._type, parameter_mode=mode, export=self._export, no_templates=no_templates, update_mode=self._update_mode, idx=self._idx, eval_helper=None, fixed=final_sub) param.based_on = self else: param = self return param, changed @staticmethod def fix_export_string(value): """Add missing quotes to jube_wp_envstr if needed""" env_str = "" for var_name, var_value in re.findall( r"^export (.+?)\s*=\s*(.+?)\s*$", value, re.MULTILINE): if (var_value[0] == "'" and var_value[-1] == "'") or \ (var_value[0] == "\"" and var_value[-1] == "\""): env_str += "export {0}={1}\n".format(var_name, var_value) else: env_str += "export {0}=\"{1}\"\n".format( var_name, var_value.replace("\"", "\\\"")) return env_str class TemplateParameter(Parameter): """A TemplateParameter represent a set of possible parameter values, which can be accessed by a single name. To use the template in a specific environment, it must be expanded.""" @property def value(self): """Return Template values""" return self._separator.join(self._value) def expand(self): """Expand Template and produce set of static parameter""" if (self._idx is None) or (self._idx == -1): indices = range(len(self._value)) else: indices = [self._idx] for index in indices: value = self._value[index] static_param = StaticParameter(name=self._name, value=value, separator=self._separator, parameter_type=self._type, export=self._export, update_mode=self._update_mode, idx=index) static_param.based_on = self yield static_param class FixedParameter(StaticParameter): """A FixedParameter is a parameter which can not be evaluated anymore. It represents a fixed value. """ def __init__(self, name, value, separator=None, parameter_type="string", parameter_mode="text", export=False, update_mode=NEVER_MODE, idx=-1, eval_helper=None): StaticParameter.__init__(self, name, value, separator, parameter_type, parameter_mode, export, update_mode, idx, eval_helper) self._depending_parameter = set() def substitute_and_evaluate(self, parametersets=None, final_sub=False, no_templates=False, force_evaluation=False): """No substitute""" return self, False JUBE-2.2.2/jube2/info.py0000664000175000017500000003027313426051426014456 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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.XMLParser(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, pretty=not parametrization_only_csv, 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 FINISHED or "RUNNING" dependign on the workpackage status""" all_done = True for step_name in benchmark.workpackages: for workpackage in benchmark.workpackages[step_name]: all_done = workpackage.done and all_done if all_done: print("FINISHED") else: print("RUNNING") JUBE-2.2.2/jube2/pattern.py0000664000175000017500000002565413426051426015207 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 . """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): self._derived = pattern_mode != "pattern" if not self._derived: pattern_mode = "text" self._default = default # 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 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) 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) 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 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 JUBE-2.2.2/jube2/jubeio.py0000664000175000017500000020737213426051426015006 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 try: import queue except ImportError: import Queue as 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 sys import re import copy import hashlib import jube2.log from distutils.version import StrictVersion LOGGER = jube2.log.get_logger(__name__) class XMLParser(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 @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)) try: tree = ET.parse(self._filename) except Exception as parseerror: raise IOError(("XML parse error in \"{0}\": {1}\n" + "XML is not valid, use validation tool.") .format(self._filename, str(parseerror))) # 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))) XMLParser._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 = \ XMLParser._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) XMLParser._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._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 run.") # Rerun removing invalid tags LOGGER.debug(" Remove invalid tags") LOGGER.debug(" Available tags: {0}" .format(jube2.conf.DEFAULT_SEPARATOR.join(self._tags))) XMLParser._remove_invalid_tags(tree.getroot(), self._tags) # Check tags for element in tree.getroot(): XMLParser._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""" if tags is None: tags = set() tag_tags_str = element.get("tag") if tag_tags_str is not None: # Check for old tag format if "," in tag_tags_str: tag_tags_str = XMLParser._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 + '(?=\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(element.get("tag"))) else: return True @staticmethod def _remove_invalid_tags(etree, tags): """Remove tags which contain an invalid tags-attribute""" children = list(etree) for child in children: if not XMLParser._check_valid_tags(child, tags): etree.remove(child) continue XMLParser._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 XMLParser._check_valid_tags(child, self._tags)): filename = XMLParser._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() 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: self._preprocessor(child) 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) 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 = ET.parse(file_path).getroot() XMLParser._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 = XMLParser._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 = XMLParser._attribute_from_element( analyser_etree, "name") analyse_result[analyser_name] = dict() for step_etree in analyser_etree: XMLParser._check_tag(step_etree, ["step"]) step_name = XMLParser._attribute_from_element( step_etree, "name") analyse_result[analyser_name][step_name] = dict() for workpackage_etree in step_etree: XMLParser._check_tag(workpackage_etree, ["workpackage"]) wp_id = int(XMLParser._attribute_from_element( workpackage_etree, "id")) analyse_result[analyser_name][step_name][wp_id] = dict() for pattern_etree in workpackage_etree: XMLParser._check_tag(pattern_etree, ["pattern"]) pattern_name = \ XMLParser._attribute_from_element( pattern_etree, "name") pattern_type = \ XMLParser._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.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(): XMLParser._check_tag(element, ["workpackage"]) # Read XML-data (workpackage_id, step_name, parameterset, parents, iteration_siblings, iteration, cycle, set_env, unset_env) = \ XMLParser._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: XMLParser._check_tag(element, valid_tags) workpackage_id = int(XMLParser._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 = XMLParser._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 = XMLParser._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: XMLParser._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"] for element in include_path_etree: XMLParser._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") 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)) def _read_envvar_include_path(self): """Add environment var include-path""" LOGGER.debug(" Read $JUBE_INCLUDE_PATH") if "JUBE_INCLUDE_PATH" in os.environ: self._include_path += \ [include_path for include_path in os.environ["JUBE_INCLUDE_PATH"].split(":") if include_path != ""] def _create_benchmark(self, benchmark_etree, global_parametersets, global_substitutesets, global_filesets, global_patternsets): """Create benchmark from etree Return a benchmark """ name = \ XMLParser._attribute_from_element(benchmark_etree, "name").strip() valid_tags = ["parameterset", "substituteset", "fileset", "step", "comment", "patternset", "analyzer", "analyser", "result"] for element in benchmark_etree: XMLParser._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 = XMLParser._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 = \ XMLParser._combine_global_and_local_sets( global_parametersets, self._extract_parametersets(benchmark_etree)) substitutesets = \ XMLParser._combine_global_and_local_sets( global_substitutesets, self._extract_substitutesets(benchmark_etree)) filesets = \ XMLParser._combine_global_and_local_sets( global_filesets, self._extract_filesets(benchmark_etree)) patternsets = \ XMLParser._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 = XMLParser._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 = XMLParser._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()) 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) for element in etree_step: XMLParser._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() 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") 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) step.add_operation(operation) elif element.tag == "use": step.add_uses(XMLParser._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 = XMLParser._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 = XMLParser._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: XMLParser._check_tag(element, valid_tags) if element.tag == "analyse": step_name = XMLParser._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(XMLParser._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"] 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: XMLParser._check_tag(element, valid_tags) if element.tag == "use": uses.append(XMLParser._extract_use(element)) elif element.tag == "table": result = XMLParser._extract_table(element) result.result_dir = result_dir elif element.tag == "syslog": result = XMLParser._extract_syslog(element) if element.tag in ["table", "syslog"]: 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 = XMLParser._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"]: 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: XMLParser._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_syslog(etree_syslog): """Extract requires syslog information from etree.""" name = XMLParser._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: XMLParser._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 _extract_extern_set(self, filename, set_type, name, search_name=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 = ET.parse(file_path).getroot() XMLParser._remove_invalid_tags(etree, self._tags) 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) 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) if set_type == "parameterset": if result_set is None: result_set = jube2.parameter.Parameterset(name) 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 = XMLParser._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 parameterset = self._extract_extern_set(parts[0], "parameterset", name, search_name) else: parameterset = jube2.parameter.Parameterset(name) 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: XMLParser._check_tag(param, ["parameter"]) name = XMLParser._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" if parameter_mode not in \ set(["text"]).union(jube2.conf.ALLOWED_SCRIPTTYPES): 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) parameters.append(parameter) return parameters def _extract_patternsets(self, etree): """Return patternset from etree""" patternsets = dict() for element in etree.findall("patternset"): name = XMLParser._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 XMLParser._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: XMLParser._check_tag(pattern, ["pattern"]) name = XMLParser._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() 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)) return patternlist def _extract_filesets(self, etree): """Return filesets from etree""" filesets = dict() for element in etree.findall("fileset"): name = XMLParser._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 = XMLParser._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: XMLParser._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 = XMLParser._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 = XMLParser._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: XMLParser._check_tag(sub, valid_tags) if sub.tag == "iofile": in_file = XMLParser._attribute_from_element(sub, "in").strip() out_file = XMLParser._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 = "" + \ XMLParser._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: <{0}>").format(element.tag)) JUBE-2.2.2/jube2/conf.py0000664000175000017500000000437113426051426014450 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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.2.2" ALLOWED_SCRIPTTYPES = set(["python", "perl", "shell"]) 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" 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 JUBE-2.2.2/jube2/log.py0000664000175000017500000001301013426051426014272 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 JUBE-2.2.2/jube2/fileset.py0000664000175000017500000002530513426051426015156 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 JUBE-2.2.2/jube2/util/0000775000175000017500000000000013426052212014113 5ustar sebisebi00000000000000JUBE-2.2.2/jube2/util/output.py0000664000175000017500000001730313426051426016037 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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, pretty=True, 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 not 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 pretty: 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 pretty: wraps.append(text.splitlines()) else: wraps.append([text.replace("\n", " ")]) grow = True height = 0 while grow: grow = False line_str = " " * indent 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 pretty: if i < len(max_length) - 1: if separator is None: line_str += " | " else: line_str += separator else: if i < len(max_length) - 1: if separator is None: line_str += "," else: line_str += separator 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) JUBE-2.2.2/jube2/util/__init__.py0000664000175000017500000000144213426051426016233 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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""" JUBE-2.2.2/jube2/util/util.py0000664000175000017500000003253513426051426015460 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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) try: import queue except ImportError: import Queue as queue 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 WorkStat(object): """Workpackage queuing handler""" def __init__(self): self._work_list = queue.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.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 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 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 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 = re.sub(r"(\$\$)(?=(\$\$|[^$]))", "$$$$", 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 tmp.safe_substitute(str_substitution_dict) def convert_type(value_type, value, stop=True): """Convert value to given type""" result_value = None try: if value_type == "int": if value == "nan": result_value = float("nan") else: result_value = int(float(value)) elif value_type == "float": result_value = float(value) 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 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 + "\"" sub = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 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] JUBE-2.2.2/jube2/workpackage.py0000664000175000017500000010017713426051426016022 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 xml.etree.ElementTree as ET import jube2.util.util import jube2.util.output import jube2.conf import jube2.log import jube2.parameter import os import stat 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 @property def cycle(self): """Return current loop cycle""" return self._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 add_parent(self, workpackage): """Add a parent Workpackage""" self._parents.append(workpackage) @property def parameterset(self): """Return parameterset""" return self._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 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): os.mkdir(shared_folder) # 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): """Run all available operations""" continue_op = True continue_cycle = True 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)) # 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)) # 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)) self.operation_done(operation_number, True) if not continue_op or not continue_cycle: break return continue_op, continue_cycle def run(self): """Run step and use current parameter space""" # Workpackage already done or error? if self.done or self.error: return continue_op = True continue_cycle = True while (continue_cycle and continue_op): stepstr = ("{0} ( iter:{2} | id:{1} | parents:{3} | cycle:{4} )" .format(self._step.name, self._id, self._iteration, ",".join([parent.step.name + "(" + str(parent.id) + ")" for parent in self._parents]), self._cycle)) 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: 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): os.makedirs(alt_work_dir) # 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) # --- 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 re: self.set_error(True, str(re)) continue_cycle = False if jube2.conf.EXIT_ON_ERROR: raise(RuntimeError(str(re))) else: LOGGER.debug( "{0}\n{1}\n{2}".format(40 * "-", str(re), 40 * "-")) @staticmethod def reduce_workpackage_id_counter(): Workpackage.id_counter = Workpackage.id_counter - 1 JUBE-2.2.2/jube2/__init__.py0000664000175000017500000000143513426051426015260 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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""" JUBE-2.2.2/jube2/main.py0000664000175000017500000010067713426051426014455 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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""" 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)) # Read existing benchmark configuration try: parser = jube2.jubeio.XMLParser(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.XMLParser(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.XMLParser(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 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)) # 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.XMLParser(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 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) # 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 tags = args.tag if tags is not None: tags = set(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.XMLParser(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 _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"} } } # 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"} } } # 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", "func": remove_benchmarks, "arguments": { ('dir',): {"metavar": "DIRECTORY", "nargs": "?", "help": "benchmark directory", "default": "."}, ("-i", "--id"): {"help": "use benchmarks given by id", "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() JUBE-2.2.2/jube2/benchmark.py0000664000175000017500000007077613426051426015471 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 xml.etree.ElementTree as ET import xml.dom.minidom as DOM import os 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 substitutesets(self): """Return substitutesets""" return self._substitutesets @property def workpackages(self): """Return workpackages""" return self._workpackages 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 @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): """Run analyser""" if show_info: LOGGER.info(">>> Start analyse") 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): """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() 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)) # 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) 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() if not workpackage.done: workpackage.run() 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) # 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 _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) JUBE-2.2.2/jube2/step.py0000664000175000017500000006457513426051426014512 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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 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): 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 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) 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 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 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) # 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): self._do = do 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 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): """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) # Create directory if it does not exist if not jube2.conf.DEBUG_MODE and not os.path.exists(work_dir): os.makedirs(work_dir) if not only_check_pending: abs_info_file_path = \ os.path.abspath(os.path.join(work_dir, jube2.conf.ENVIRONMENT_INFO)) # 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 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: print(read_out.decode(errors="ignore"), end="") try: stdout.write(read_out) except TypeError: stdout.write(read_out.decode(errors="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) # 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 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._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): """Read standard environment info file in given directory.""" env = dict() last = None env_file_path = os.path.join(work_dir, jube2.conf.ENVIRONMENT_INFO) 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 JUBE-2.2.2/jube2/help.py0000664000175000017500000000316413426051426014452 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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() JUBE-2.2.2/jube2/completion.py0000664000175000017500000000637513426051426015702 0ustar sebisebi00000000000000# JUBE Benchmarking Environment # Copyright (C) 2008-2019 # 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) JUBE-2.2.2/examples/0000775000175000017500000000000013426052212013745 5ustar sebisebi00000000000000JUBE-2.2.2/examples/scripting_parameter/0000775000175000017500000000000013426052212020007 5ustar sebisebi00000000000000JUBE-2.2.2/examples/scripting_parameter/scripting_parameter.xml0000664000175000017500000000212213426051426024576 0ustar sebisebi00000000000000 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" JUBE-2.2.2/examples/cycle/0000775000175000017500000000000013426052212015044 5ustar sebisebi00000000000000JUBE-2.2.2/examples/cycle/cycle.xml0000664000175000017500000000046713426051426016702 0ustar sebisebi00000000000000 A cycle example echo $jube_wp_cycle touch done JUBE-2.2.2/examples/include/0000775000175000017500000000000013426052212015370 5ustar sebisebi00000000000000JUBE-2.2.2/examples/include/include_data.xml0000664000175000017500000000052213426051426020533 0ustar sebisebi00000000000000 1,2,4 Hello echo Test echo $number JUBE-2.2.2/examples/include/main.xml0000664000175000017500000000134013426051426017042 0ustar sebisebi00000000000000 A include example bar param_set param_set2 echo $foo JUBE-2.2.2/examples/iterations/0000775000175000017500000000000013426052212016126 5ustar sebisebi00000000000000JUBE-2.2.2/examples/iterations/iterations.xml0000664000175000017500000000253413426051426021043 0ustar sebisebi00000000000000 A Iteration example 1,2,4 $foo iter:$jube_wp_iteration param_set echo $bar echo $bar analyse analyse_no_reduce
jube_res_analyserjube_wp_id_first_stepjube_wp_idjube_wp_iteration_first_stepjube_wp_iterationfoo
JUBE-2.2.2/examples/result_creation/0000775000175000017500000000000013426052212017147 5ustar sebisebi00000000000000JUBE-2.2.2/examples/result_creation/result_creation.xml0000664000175000017500000000234413426051426023104 0ustar sebisebi00000000000000 A result creation example 1,2,4 Number: $jube_pat_int param_set echo "Number: $number" pattern stdout analyse numbernumber_pat
JUBE-2.2.2/examples/tagging/0000775000175000017500000000000013426052212015365 5ustar sebisebi00000000000000JUBE-2.2.2/examples/tagging/tagging.xml0000664000175000017500000000121213426051426017531 0ustar sebisebi00000000000000 Tags as logical combination Hello Hallo World param_set echo '$hello_str $world_str' JUBE-2.2.2/examples/parameterspace/0000775000175000017500000000000013426052212016741 5ustar sebisebi00000000000000JUBE-2.2.2/examples/parameterspace/parameterspace.xml0000664000175000017500000000122313426051426022463 0ustar sebisebi00000000000000 A parameterspace creation example 1,2,4 Hello;World param_set echo "$text $number" JUBE-2.2.2/examples/shared/0000775000175000017500000000000013426052212015213 5ustar sebisebi00000000000000JUBE-2.2.2/examples/shared/shared.xml0000664000175000017500000000114413426051426017211 0ustar sebisebi00000000000000 A shared folder example 1,2,4 param_set echo $jube_wp_id >> shared/all_ids cat all_ids JUBE-2.2.2/examples/statistic/0000775000175000017500000000000013426052212015754 5ustar sebisebi00000000000000JUBE-2.2.2/examples/statistic/statistic.xml0000664000175000017500000000263113426051426020515 0ustar sebisebi00000000000000 A result reduce example $jube_pat_int echo "1 2 3 4 5 6 7 8 9 10" pattern stdout analyse number_patnumber_pat_lastnumber_pat_minnumber_pat_maxnumber_pat_sumnumber_pat_cntnumber_pat_avgnumber_pat_std
JUBE-2.2.2/examples/files_and_sub/0000775000175000017500000000000013426052212016542 5ustar sebisebi00000000000000JUBE-2.2.2/examples/files_and_sub/file.in0000664000175000017500000000002113426051426020010 0ustar sebisebi00000000000000Number: #NUMBER# JUBE-2.2.2/examples/files_and_sub/files_and_sub.xml0000664000175000017500000000174013426051426022071 0ustar sebisebi00000000000000 A file copy and substitution example 1,2,4 file.in param_set files substitute cat file.out JUBE-2.2.2/examples/parameter_update/0000775000175000017500000000000013426052212017267 5ustar sebisebi00000000000000JUBE-2.2.2/examples/parameter_update/parameter_update.xml0000775000175000017500000000207313426051426023346 0ustar sebisebi00000000000000 A parameter_dependencies 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 JUBE-2.2.2/examples/scripting_pattern/0000775000175000017500000000000013426052212017504 5ustar sebisebi00000000000000JUBE-2.2.2/examples/scripting_pattern/scripting_pattern.xml0000664000175000017500000000371413426051426024000 0ustar sebisebi00000000000000 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
JUBE-2.2.2/examples/parameter_dependencies/0000775000175000017500000000000013426052212020433 5ustar sebisebi00000000000000JUBE-2.2.2/examples/parameter_dependencies/include_file.xml0000664000175000017500000000043613426051426023610 0ustar sebisebi00000000000000 10 20 JUBE-2.2.2/examples/parameter_dependencies/parameter_dependencies.xml0000664000175000017500000000177313426051426025661 0ustar sebisebi00000000000000 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" JUBE-2.2.2/examples/dependencies/0000775000175000017500000000000013426052212016373 5ustar sebisebi00000000000000JUBE-2.2.2/examples/dependencies/dependencies.xml0000664000175000017500000000125113426051426021550 0ustar sebisebi00000000000000 A Dependency example 1,2,4 param_set echo $number cat first_step/stdout JUBE-2.2.2/examples/jobsystem/0000775000175000017500000000000013426052212015764 5ustar sebisebi00000000000000JUBE-2.2.2/examples/jobsystem/job.run.in0000664000175000017500000000034513426051426017701 0ustar sebisebi00000000000000#!/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# JUBE-2.2.2/examples/jobsystem/jobsystem.xml0000664000175000017500000000375713426051426020547 0ustar sebisebi00000000000000 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 JUBE-2.2.2/examples/environment/0000775000175000017500000000000013426052212016311 5ustar sebisebi00000000000000JUBE-2.2.2/examples/environment/environment.xml0000664000175000017500000000147413426051426021413 0ustar sebisebi00000000000000 An environment handling example VALUE export SHELL_VAR=Hello echo "$$SHELL_VAR world" param_set echo $$EXPORT_ME echo "$$SHELL_VAR again" JUBE-2.2.2/examples/hello_world/0000775000175000017500000000000013426052212016257 5ustar sebisebi00000000000000JUBE-2.2.2/examples/hello_world/hello_world.xml0000664000175000017500000000077413426051426021331 0ustar sebisebi00000000000000 A simple hello world Hello World hello_parameter echo $hello_str JUBE-2.2.2/PKG-INFO0000664000175000017500000000360313426052212013226 0ustar sebisebi00000000000000Metadata-Version: 1.1 Name: JUBE Version: 2.2.2 Summary: JUBE Benchmarking Environment Home-page: www.fz-juelich.de/jube Author: Forschungszentrum Juelich GmbH Author-email: jube.jsc@fz-juelich.de License: GPLv3 Download-URL: www.fz-juelich.de/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 :: 2.6 Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Benchmark Classifier: Topic :: Software Development :: Testing JUBE-2.2.2/MANIFEST.in0000664000175000017500000000030313426051426013667 0ustar sebisebi00000000000000include LICENSE include RELEASE_NOTES include docs/JUBE.pdf recursive-include examples * recursive-include contrib/schema * recursive-include platform * include bin/jube include bin/jube-autorun JUBE-2.2.2/LICENSE0000664000175000017500000010451313426051426013146 0ustar sebisebi00000000000000 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 .