From d45fd2c2aa08b5fbbc409942bca5e548e7012397 Mon Sep 17 00:00:00 2001 From: HoelzerC Date: Wed, 21 Aug 2024 17:15:48 +0200 Subject: [PATCH] Initial version 1.0 --- LICENSE | 408 +++++++++++++++ README.md | 50 ++ assets/logo.png | Bin 0 -> 150384 bytes scripts/train.py | 316 ++++++++++++ setup_environment.sh | 18 + src/data/__init__.py | 1 + src/data/datasets.py | 318 ++++++++++++ src/models/DimeNetPP/__init__.py | 1 + src/models/DimeNetPP/wrapped.py | 59 +++ src/models/MACE/__init__.py | 1 + .../MACE/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 245 bytes .../MACE/__pycache__/wrapped.cpython-311.pyc | Bin 0 -> 6072 bytes src/models/MACE/wrapped.py | 129 +++++ src/models/SchNet/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 251 bytes .../__pycache__/wrapped.cpython-311.pyc | Bin 0 -> 2106 bytes src/models/SchNet/wrapped.py | 47 ++ src/models/__init__.py | 0 src/models/base.py | 91 ++++ src/models/reference_energy.py | 143 ++++++ src/training/lightning.py | 477 ++++++++++++++++++ src/training/metrics.py | 335 ++++++++++++ src/transform/__init__.py | 8 + src/transform/base_processor.py | 26 + src/transform/base_transform.py | 28 + src/transform/filter_rmsd.py | 18 + src/transform/graph_construction.py | 27 + src/transform/pipeline.py | 14 + src/transform/rename.py | 30 ++ src/transform/scale.py | 30 ++ src/transform/units_conversion.py | 65 +++ src/util/__init__.py | 1 + src/util/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 161 bytes .../__pycache__/deployment.cpython-311.pyc | Bin 0 -> 4803 bytes .../periodic_table.cpython-311.pyc | Bin 0 -> 4787 bytes src/util/__pycache__/units.cpython-311.pyc | Bin 0 -> 469 bytes src/util/deployment.py | 104 ++++ src/util/periodic_table.py | 137 +++++ src/util/units.py | 27 + 39 files changed, 2910 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/logo.png create mode 100644 scripts/train.py create mode 100644 setup_environment.sh create mode 100644 src/data/__init__.py create mode 100644 src/data/datasets.py create mode 100644 src/models/DimeNetPP/__init__.py create mode 100644 src/models/DimeNetPP/wrapped.py create mode 100644 src/models/MACE/__init__.py create mode 100644 src/models/MACE/__pycache__/__init__.cpython-311.pyc create mode 100644 src/models/MACE/__pycache__/wrapped.cpython-311.pyc create mode 100644 src/models/MACE/wrapped.py create mode 100644 src/models/SchNet/__init__.py create mode 100644 src/models/SchNet/__pycache__/__init__.cpython-311.pyc create mode 100644 src/models/SchNet/__pycache__/wrapped.cpython-311.pyc create mode 100644 src/models/SchNet/wrapped.py create mode 100644 src/models/__init__.py create mode 100644 src/models/base.py create mode 100644 src/models/reference_energy.py create mode 100644 src/training/lightning.py create mode 100644 src/training/metrics.py create mode 100644 src/transform/__init__.py create mode 100644 src/transform/base_processor.py create mode 100644 src/transform/base_transform.py create mode 100644 src/transform/filter_rmsd.py create mode 100644 src/transform/graph_construction.py create mode 100644 src/transform/pipeline.py create mode 100644 src/transform/rename.py create mode 100644 src/transform/scale.py create mode 100644 src/transform/units_conversion.py create mode 100644 src/util/__init__.py create mode 100644 src/util/__pycache__/__init__.cpython-311.pyc create mode 100644 src/util/__pycache__/deployment.cpython-311.pyc create mode 100644 src/util/__pycache__/periodic_table.cpython-311.pyc create mode 100644 src/util/__pycache__/units.cpython-311.pyc create mode 100644 src/util/deployment.py create mode 100644 src/util/periodic_table.py create mode 100644 src/util/units.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe463e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,408 @@ +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..add73a6 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# ConfRank + +[![Python](https://img.shields.io/badge/python-3.11.5-blue.svg)](https://www.python.org) +[![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +This is the official repository of the `ConfRank` project developed by the [Grimme](https://www.chemie.uni-bonn.de/grimme) and [Fraunhofer SCAI-VMD](https://www.scai.fraunhofer.de/en/business-research-areas/virtual-material-design.html) groups in Bonn. + + +
+ConfRank +
+ + +# Software Setup + +You can create the environment using the following command: + +```bash +bash setup_environment.sh +``` + +To activate the virtual environment simply run: + +```bash +conda activate confrank +``` +The current setup is tested with python version 3.11.5 and CUDA 11.8. + + +# Data + +The data is available under: [https://zenodo.org/records/13354132](https://zenodo.org/records/13354132) + + +# Citations + +When using or referencing to the `ConfRank` project please cite: +- **tbd** + + +# License + +[![CC BY NC 4.0][cc-by-nc-image]][cc-by-nc] + +This work is licensed under a +[Creative Commons Attribution-NonCommercial 4.0 International License][cc-by-nc]. + + +[cc-by-nc]: http://creativecommons.org/licenses/by-nc/4.0/ +[cc-by-nc-image]: https://i.creativecommons.org/l/by-nc/4.0/88x31.png diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..55898cee2e479068dc9d02330ee5b90e2cfeb41c GIT binary patch literal 150384 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Âavvx1jCL>B zTep~h(6yBum)sVzL}^4q1>rj=5wWPi2Qklov}(OhF`Y?til=J!OF{`qc0=+(VD1FL znB8t4FTp)8lbIrkP~z`=|V$%N5s7rgHk+FQW-cl z;J^aa=@7uU&3M`hwG5ExwhLjn_Fy@H@a%6d)n7}1!was6IjBqoaTTJ05IP)(t!1^t z^l`~M-)g+OtSyZ1vE%d(?$Owjm7Oh#-cgOi{ps?edn3DOT}(sRyFGXMq4TEq>)&j1 zO}qP1A4RnRnz=uFKiA@k3svbOOv_Ww$5*=Z6}0cPeiTv_5p>taNNQyiDjqM#DtdBc zzm=BI_;1Ma$v9kQ$R@?~{4?n2HhSMXUAQ&qcnVL+BwtBIZ)&<)gyLj9_SqR45Kqep#Ui-D9rMpjmg_gRM$Dy=hiux@1;0(}GcbQn2r z1PqN^byMdNam?zR7Qb>?HThKtvoF)R&G8j;Vw%p^Z9#Ac&BcJ>Ht0ku=FGuqYy{14 zAmxW*CX8%P8}KUWe$B+u?ezWl$I52?RERV&=)XrfOa#6aekz#7|NBFPTw`}=ggpbI@| z4SMnlPpOn*Yh|@;OsNR5Cu1)TbKUe17kC{R#OT70?dyEyN8e~0pjP7qeS+BMx za<%HPJ2{L=HJgKATO)9!fZ3-^yG$Hcp)qQ|u;MC%W z#4RQ&J~457nQ>1RXGbGcpCacKtRGujcA&clUk(5) zRl4m|Rww&J1z)~M;-PhQO@i?sL;{@9Oc~Zb-B(8A%k{y__^zc|=i=6o+0OmDXITt+ zVuMA^Bw8q1Us_!Kc>=7TIG7#}X;|4oVyEMC#Tak1^b;9gR%-xeAGQ1W)ou&tOSKF3F1wC(CKAC3 z%6mP?HS@#oR7ev{1P}vx)xz|BpvVuAN}OE6vJXBaubpFZi@mV|V@MJ__5{{kb`0h^ zFo>P4+xj5Zh|v-|k=rQ!8<5)6Dpqlaac(h2BQ|(sRexZnC@aqepS+jVDhQqPF0(HW z&S-Aa8=mSMl^{H?wv9bJQfGJiU{PLfxn18rB1N(nbq(iLc1#5vDD#<>2A8N4hvHiE zs(qz&eV0ZM#&3rbf$Ve)q)KNto_JGJ_OYkGttc$o?ky3;40RTeQ@C>L+K(D1z%UX7 z`^<}*;CX_N9O7kZL`10I4F=gj2l=MebWD+t1bM@UH=9{G8GT=``!|KvU08Blx}AEx z^GCl+;Tx0fe>svYpZCHZ`2d~QG?0`DAl=}z7nhRC99czxRsiKIv;YVOe-WA}=)&K? z{F3ImBeQbU-U8z?QdqLxyLe)91!oP({*L8O;bu>Q=}tv zp!s1dI-a*814M|N93nf3F7SHw>XlEb#j+jBvtvJK#+?ZSm2v__a#t`J6gXPK%&pjt zlX@PcdpKAtk@l`Fy??s4Q_Mvz6~8@&)#Z}1gzb-Y^`>>@$I+0iWjiBZ_Icvk02|-j zTT+)8()T7bSV*4D(uk22X*G*%yzXs{!WKQWWzEwc9!l3Ok&UgeCOa55*1wtcX=j-F z#tp%l1XSDMrC)V*jE*mW7vR`Lx)BQ13St*_+#BF=g6$l{3c$yJIglxi3S7i0`ZTsp z2WT)h!&KO3g2<|^{*=5$1Oet7a6E#3>doPlPkM#rNSWQtGw_}PUIwfrNUcjXSU~Ot zuQFK9;hB>R<4fci=lE{VxgylFOh^mu?zV0=<>iV;UuTqEoDUc+D0S&e*e0 z&X-xU4!(Hf%)r)8KqLzosI;8lb-qhc3U)J&aXV~k31h;tC?D~ldZUsY{8`!=v!X<|IVCpoU-?Af zPO-}{-@56blr7Lql#t_d{Ap`Unswhyzvki!}>4v@Uljngel*D`1EqIPfA~RnB{Xq>jeM$IHIWp0LTu&%Xg| z9ze}YyPtlV0rd{M&@YRM7sUBL&47^ySDH4fdO}k_OWK6Z=I$`%4VcV;jP2W5m^yCD z|BtU%WPWw8fA1Au0we%I5LHWmssPyU!KlRUbKwblmD~CLh|0sjZwaSvvC2iZR@q;= z-fEG*?O1=x`j?3!=BI7Wn!^69oo4K9^zZw!jei?({AlnIX--rRkTJ!@43A8(>TTKd z+)s3fa0&42(F9fsB~>5cnwG5m}?i_{7b2NmpYz;~zu zIs+Iil!#1&GFLe7Y2Hui>^RU4sFf$QFdUS-To!A(omL=NUCw^J(@9rbM@Kn$20^Cz z$CT+(vBd)5J;?o9+TFe!71%uD>vpi#r*-gsWDYCCz?tAFF|KnSjU`-?9-SV$*1uFC!WF*x%BwK zjBo#SMX>ZB$N}&Y>m!#CHxA`Yv!xxB2=&Jhmv{IScjb<@_wj<@ZZ8+2@E6#qz7E#| zOAnGSijYhoqcUf7arypZa^>kSB2I_?1ZHi(1PXOdS(j4X2$|U!P)(yzSTQMB`k%X~ zr`IRjSBo(r7ZkE)0i=W4ucoP<5^uxb+S$6j80_u^^l?4O;N(TBSoL}fcI zNrrq$Vsm64)gO5U`DY?3&8^eI}LvhLifKgqeoh4S0o{R!=ZRrNq|(mnBo z%v!-k9lK%g80dA3=$O-oVFJR9LE8Ly_+_i{TTuK_KuRzos)muwu0|Vnk-253*&7LW zUMh)weHYyEvT*8mTO}LiY=|81PLGGrTRN}2TjWsXwXvaw_V{2QC7=~e9}CosOmu9a zDmNKDRPH#}A?DzKfLG1xN zWrxCfSqr7fN$+{2T#?$)?}erE^~ds*K0CUMIh^V#=Cc=eB$cKeI z?|4?lo=fp!I4S0i=pb$El;8V?J=fdxZ6mP(ig)+#DR%GPiq8}Ba3{_0kjP+AzWni2 zG`5fXt4REc?TY>!RUx%>Jz86mEbp5_ZOL^)p z7%>ohREx@YIGS3ToJ%SwMs}PKeHVMywQX0(rXj}{vsNL5v>F6FglAf<0P1`VN zgE$>p)*uECS@cJ(00+!wah#G*f#P}+T+s1p*1K}RV~G%LXUkF|Px8h-LM-Edr+mhT zT#p7Fz!4w;S@x+*85c|W5Paa1ZcFz7%YvN&RxiZ+{p{@^^}Bo#+1^fz*7;lQBpPg5&1;SDg$1TcnJO%29AU}gfy+4&ScQu{wm9g+>61zT|B*t)~p@=lZ zcRUV?^9lI6-J!q9r0UuAS1DQJrxIR5)R(2bF8cb0iW$DJd>;(XTGoEWj@;*S|H|0m?Ef_xF6dQvFTL>t1hx!8yvp9n5I|M3w;@~? z?V3*6uL6!9{WH{U|ETrx6u9to{h!AlZ%$pN6*TVNgMeTgp$}R`NF}Jcj+?(f&RzJ{x7`~M82Z&K@YH5!%>F8QKvqd8 z1`alWAK=o1O<4?XRB|4xX^A~9{LJ>QpBFk*#0>Nj#w=y<_RkLXrK+k(6JUE^8!)=1IRtJHq&RuuO)2$ zGow~0$(=7yO=O_j`Y;pxtIe7zr=3BCwNWLG~YL+PT2hLL+cHnhpgZ zCxY-mO|`#b&hy^yQ17zi1z}P!=uOW>iY!7}L<`7mWCp!yAC+q00{cma-@NV|1D0i2 zI80NWh#^eGXm_y>v7$kOypiVTy1HaYCxg}j@V(LU^13wP4-)u?vNe9VJR7*2hnoy> zZECOt^ekjR-`eKd&96%60gr;nA)*F;tWN-TfMN}9BA7OF(!G;FR}?ZnK7OBAAh^wd zTgOKz#(Djk9}OyJ$32dcC5Bqh^V}(|W}Qb3w#0MWdG@8+KmiU#u77!>4ROf*N?QmCgwb6uZe{x{Pl!0t`K=HBT22Uc&Lm&UT$Vp zRRZwzpi2MVDr2Qt7atu{m9M8k`q%R1(@xz84NpughLLXFkmm`n`)*bKay($}5%zEt z4$X-#bAB~fN~Iyv$+>3sPyb-~%rET~d#RnYZ?(+A=(+n+*N!?)a~9mJ*Ut3YRdZJ&}2Z&gW*L2&1RuHE_c`>{?V&;Wbip7!SoxoT0EV)6h;ss zL<@ce(Kk7dS?7qeX-Df@AAmV#9!TNO85!3?+rGo02Z5~!Ed^0@H*jzuByktwEd&bJ zy-M9569`}MMR^tEzDY|H$HQzjPJENXXu9fopqbxzDAX>W!WSl0RVAx{m&@3b1ODud z4I2>efEX{MxH$4o_Gs@1cYD$lA7hzWR7So;hQYtb%lo*qc7pv3!l{J!6Ms?R}X3uDyMevH1Us3E>d} z6k_k-045~t+!wzBT4F6LTHi1#n{%T2KX9DQ5BRj3QRsMN0HlUgr#0AK;e z29E&@DvZp`$l=pdMwO~{!FCC*&O)_S6jRq%4HnPSv8lyw z=3*!y(JR$ZtR4Iz#!)Cb4(48sDw})%HCIUsQo=9@B6C0p9bH@;;NU~`Jsx3q*|?Zg zDq|UfL(pr4)i>Qcq}~t{)K~=9BRW2Px#ip3dRP{w9*T>p=_o9X{G?- zD+|x8t*q!oThC~H2JJOX{grB zg#Mi;hikDW^5(UPYfOQ|7453G*ZH+CA_bSivubsL*-h?=w*n7IBCX7FZKA4q z=H!${AOH|PXFvv&<)xgM!yZvqbBg1A{6=grx@HpJH+jjgE8Ui(pjlX?@?2F@9y*#S z6j@^F{GK#T6?bk2w&)v_l89&W#ZWrDc@-Ze5%ouhVBHYt0DNYR1emx(?G z?k{kGK#RREsIGPgL`=u4QRD8bMY2BH(TE2K;qrfo-QEV2kqf{v7{=h5?=UER9~0x( ztOwzR@H=HN;zxD>u?OG#7Vs4rknX>f;(+*UAwv#G3<4}P7$dcvM~#6MHMCc}HSfB9 zsKH(+sttn%JmK)KWU3X!naE1Z|9Sb42&@5!I~*o@q_2Hw1!!4At-5&g0yPdYWjXvH zT6^w%H0v%-SFNf_Ylb};ov`9gl8lI)m(cqS!EsuD$!x_C5 zx@iNq@#;MMNVpC+o20wTGXAoB0q+EJ=F@E(&0*3?Wkm zC~SzTmxJ2{8q!F8XbltlVF?>;^b$}K+(w%^-*CB2W(bKZK4)`O$YJ6NoEs@<=Ruf^ z#4-xKz=Xa`N_fu;imNpJr8a&5a-W2FO`-y z#{G*I()Lg!>tN>aDbv;D#5d?V#M`mun-r6Ynk~t-Szl=Ov6)RPSlT~58#WEQKV~rP z5n1o&fptD;65hYmOn&!(JoaX*VX;rQ&t&b<$Bqs{Q@ak{oLz9?^UtDssdPK{O%;|8 zYpN=FN^)Al*uWp-@n-_c*>1mz5`P=osPfcYlI>dXng+$q)k^ZwAMEid zJGLcT^(gq50iJ|P54b&1J&PfHAN{N)y4TdKpA_PIdX_VvPvC$Xu%$NiX~RQ33)S| z&Yx;Le{VQDe8>xp=8!K4SH8QuJ1AL!pC=9@|0~Bw7gsE4_BtiHMxnG4%s|Cjjqdc7@h6C zDzDl9o(Ip=+zq>xVjX_^Yim0!3`_5(W}7ZdnZHOsfZ-BEpRg=Kj;T!a2SKL?mLku$ zBX6&ga(QED&N%>UbY?Q_JbNM8=%L8=x&b9@L#ouZJf!Kmfd!8cJWKG&AnoQbZNTaQ zKvbK7%3@B}M|$GWndKGzjF#r*H7n;HPD=c)>ue=MdeaFmll+BmLs)tQ!OgD?7A9Gf z9v}hXL4vMsh~P6iI}+C{0;P64RcISDc7jp?x@b;s&H~IE%MQU@K+EBGIy*WddaJq# zXg7g=URZ2^xds6^I$fC`CI$-jDo_t0&C&x)a>jE3@8QXTKIC8^{Fio_`nRF>go8xh z8y*@X%|)2Azgh7xM@pqCJ~1NIFr&I+9)qCF96E9d{%t*rw*U)ji~dC7FZx!l3t#?~s_3PR8Cc_x@_Dr1Au>6pKu1PCXiZ6$h&mP$bgc;e zEX*rp6Xha2{B!clvm!M^PI|)okBG^=eL!mEqDG5lzL*3Rw1V^gE4DXqiy%7A0f%pEUI4t!h;*mB?<-*Xo#P%mDLe`Ryr zeGVD|K8p6uL*}XLr^XCIl#$UXQO1dS?4{Fbbd-_*OFC(tLm87Z1qA3n%5kil0I#J^ zamVH%1DWz+-XlO`;g^I%4VJNxIZsByXVG+7n&phexq55!U11+M{X@EC(KHubmeM1A zR@JzD-z14|nc$(tTXN3_)1ov@&aJCdMBuI^IAOaaVWBLJv`&9XgYyuoqyf=E)GvSP z1W27m9*b3F#xOeZYF{vG&E62@_y&d5 zpdmaj>-eM`1nNTYI^{;1;L(;|Bf5DM+>sHMBqVnS0&H32lgr;J2@ifmH9q~_oYveC zHs{5JJ(@rrKXLPW$)vMt7Kz%kD3Xyg8zP?ufTa^vgr`@#cUi_A_60X;+$pVvD1|46 z6hbEyBAMTerDLVYyWjlHeb3m9xINBDHD16mdj?>iX%{xnUZtkTbyG_Tk0|=0Nf+vC zveT+~cz=wqf9_dnvHBHl`M6B)*`28r-jMHwZ+pJ;j5uyhi}2hW{qUTEG|nH>RO`_A z_==Fd-@nF2p6~whKv`H`i&cMtA1)cECs{ngd^KFFs2w_F zqh1E}tV906wBwVNjSWTDCg>;JbLRXa-}iFroGVk3-nYIj^lrKLxjQ(-0)Lv#Cr_O{ zKu)FBpgUH_M}}$*N3>p0#r%+ni}=xO01g|S2cB?epd7>&Mw|P+>y#@i>w^=zS^#A$EbwmExomze zD!K_xg`+7uKKEn;ta%`~|2q(Sd?33BYBN9*6aMpxwq;1i0LPKd+rN2;NJ=H{zHS8x z8b}gfb`##7H??i*dwvMXn@S?#Yj)JvcIkt&&iom%dJm5^9EM;1ne@0Fx?W)GDD{jQ zZ+42x>Qg~SXqy4-hR{=}0MBf^@q;k(C_HY`?|O@_N{vcruw#DxO88$G?dQr$;z(Y; ztW!HT!@X()BlK&4kHBpJ?cT=|S@P-vnQI0`x?`&8U^- z{q4oY7T&v;1L~FUO}TFU>4~@bQDW6>7XN5@if6ju&+yuG?No#4JR<~?WRb9}oM5T< z+s;*8-T)H%9`Mt^d}~x%k%FiNv~9E7)dtCQGWY=hs^VkclDe&9Di9-^qV29jb$nmx zzF{wk@hPZ5jR6|=Jgl1%GMg%2>^J^BUIUlSi-*G;k0a9nYXP@6D=^}K0fGMqfLstG z!BYhkh;;Nix2KnHQIk;UxZVU~ZXVaJ>e8%#XN`wI{K{3XOpk4wDE}HI6 zu($?v1A=QA#PP7{Lx&$I?x89J4+}4}EjAS5?8~4H*sl=d6o!kpz3ev|e-L4eG0*Ij8eOUg(orbNQSZp_UBWg;vz zP&G<|TXXZJG!fccm=J)j*uC;t0erf!5^sFG%6EL`hT&g{h5EJGtcoD8YDh94gEO=ebBFS;=giH1VOX~DKN zC}}_8(tdmVN}25HvNe7B@kYYQ$jDynPL1BEolf4@^dY zd;q&BnliO16-|dM+|*ctJDvAaoPKMvXni#kxon^Vj7>h7Cmczb1Lza9_8E*19j3C@ zeboxu;H`zWTqHht>f(Sm&2<(V-j{9H6!`s=ymts5K2+U(^T`D01I>77dixC7g{Wtd zl9|Z)4<-O4WqxcOuA!5Y696J*{meL9RJ66hGRUDOISIHsveuKQS@GULP=C%&|`{+1L4qC3DKAu1+Gs*tw|dVs$s11mDTra+&ho z=WgB6_5Js2-wbxGn18qWFIGfq9GlH-_QmtUZ_UaQR8&wq*QRqs9DhN*SH8)-Mfu>Hg)Ig$XaenH!-{oj@z#GY? zQY(0geJiFJHF&9shX#8M(q7{-<;4ph0+P@&J8J-ODj&MP-6RmhZv~_UEQzJ*Sr)24 zmtvHJRbE)bR~^fe%F?6jZoTX%2z_CG&Z?JC-CVynYT7#G7RY8(gI=h1Nv z|LNNG?wW{cTjiHdLZ75*-@xz#gix7b$E`NM`=es$dM?!($A6!EX*?Sjy*Hr8H+)h5 zAnrY#3`}0IOAX>B%qQboNXBci8ygKyPJW4%rn7z5MGfhzaLysmBp?@NbkfcNY0Bc% z_qhVWs90837Aq0j*eJXJkG2i^KP^B^hS*Pdtx-+_Yz?}7f33sC)V>_eM5qy8Eb{~U z7GgO-G4M62CNMS8a&>#_`)Qqq1>fM~w3>PKq=V=X7m%F;Qw7G*2F9QoZ>?+WG1cwj3K+N7k)?U= zO}6qxk3E}wmE7PW5-io|8SeS5@lZI;Q-M~d>e2ys%iHav*WTgzWIFmAv30|#5u9M_ z9?Z0=tCQuM8Yg2NZ#Sn7_b**Dlm7L$K7%;-9C)gjGJl;d2LyL(#O~hTY{v#6Q&P(#Qvb@q^n@)wJW6M!a{L zeF4-gQX{xgef&Fv(V^Y)R3yivkcdvn|fZ|0hT42Hz)0euU>wx4+g0S8ia?ZU5(**wX3AETQ?p-b(W8L+nh&a1p}n0rJ+Bo3nP9qZ6}watccv zySv>3(uNysC z%NOXqlx`A>-IrCVg@snXi**O!Vd(oGOO3hjOOO6D`Wp2=>3fkA{~o-qy7Ybbs)=N^ z*EF*2(@kdh`VasIt~`XDglrLTZ$ab>$U}%h8h|8r!owkZ(6?X@5l|8DazE06PW>aJ z>+g|~=UQ5_&}6`@{sq{tQXnP(IR>+VY)KVp)?jdjxKyMjsfV?WCGIV`NdO%|LhS3i zXf!jQz#|RD_?8)Jid$Szngs=;8V&A`flr1-OGk$i>GB5R_29jCr&emjnD>Q)Hzdr0 zJp>Ya!R1qC_LCMKJb))4jbmtK-Y4C|e@&i?cXI0y?#a2ti|;d7x@)=LH|%EYPgmBQ z^(!8bt}oMd>$0VxH10X{g{0N1IDYIv`qVnP-Oar6~M z@#Y{&bT`t6-k0avlSdyzZ%I+9#dNN4_0F4m-|U^b85eY)>Pp`?;b2fRq?(o1-5%4K z))?B@+8M+guHO^g-=>reA(ZMJb>MP4oU*Lh%8*0S5{W~9j*l01o-!=#H|P|r%97p+ zMDcrQdP~MSP6WrO+_M0{vKoT6!HERZAq=*$^GIcZD|n?5s1|;SoWerf^3lRv+4eaU zXlQ+@868sL2mhbFY!(Dw?KV{oP%!}1BM`tLc-l~F1^)?T-$MT>s2s-aT5finI;ITb z%HWxDlOY*Upc@B{dI;=Lrlt&_3xjnWMxJK?96_5Sq(^`P95xSQ$kGriJ@tcsd**Yp z%`A`%9TpYQiHUi8GgoEVP0L5kY^;|=ywCAOY7Nmn4!CERon{)2!%jk*<$CtQ}E&ZrxoNW<&yC>i1RHSltNc7))qGGVrxUpRu>v zRPE|s7S6@Xl{0qo?+ntvxhXJ8{w*8g$i(1_f^2l~70uc0ZR7|N-SB6G6hLTi0=H{M zcvYE^IoTJuQ!=0%kOk=T-*M`>y~rmj>?$GRLzn`t&t|q z&1QyQc90jm`0$7aDJTWyJfQU|zM)x3v>$Ublvq*r7r1OWty2@lPYES}N%Ky#evfCM z3AG{+5pcF&>|+e79+2Dy05J!5j%23FN{-w^m4A*;AcP1aVitZ%QGsgBZo+9~8={FG z-uMVGt=2UNzXj{y?oJOIqS*&`t#eBM_q>7kXedxfLpSJApsIKLny&qm)S!$`a8Gx^ zzd{((WK_w@B=!U`^#mjAAoOKu6(Il$s7y>vbMTF>!s3S5m$nxwz@TyKl^RWD;X8IY zYP^r-(0ytLcoG&ssw7$qw=R117eEm+*h`U-lyAwON^2C zF6ek65+AolB0TQW-RAb;lyH<7qt|922=FF|87xvHYMJhQdkk`wmoNQ?e)fyGD1I#H z==&nnK;83#@Kvtm&7VK=GOEf$y6%g|r`d>cxy}sDeSaUc97OhRRsgSzl_E(rTN;HW zeuvXhmn&|Bkf||%WLWoS2cuMty)#i{u}6@)#iQI`?!Fr)waFe>or!QgmqNTKfF}?# zxp00s!qxb#Ur61ZJs*>&`m&bYeqNf=b^5=?LnOGxJSSBW67GOjg>GgT3*p6oN=yA~ z#Sz*`OQDfoGLjPMd_)ZH5OFF;@^NXA6IR`fpShg8Tx8*hnKO^thJ=(e^er5PVMqrd zQh_0jX7M~n(HJ@q-~$f{4F#41guU=0gNzI5C~P5amB>^9a~*69vR<~9mIWEPF=ru? z1l{8BcVK{CbFMQ8sV-)68+-!Yw~PFsA}9Y*8V8U;m2;bA*Zz_q8WE*I-ix zMY-m-6-X9C?9Uoyuz}WwYGO!Pfq?`*8`yf0_d_P;0aJFq3`b(tk8~)rY6-ac?%Ag~ zgSIUvy-r^~ndoW<YA1n3q>qVd=dGA`B@iuXijx;1)vYrZSI18#wTwXHA*zB+w7^ zF91+{QpSqxXn~}r&m>_C3ntg!zQOmjo~VTe@$Lqjv$fORf6q3ev+FQFeU=52P!a} zraHf=ah{~_6ziqfKO%ey^k4Xz-_X*&tZoOS&ubG0@-0adM8f=pWdM)}3CD*@<|0m?TWHrp^lMpq zWb-V_$YhJuHWPmR8Tm~^77k_|Rnc*FP9srm{0c1vXXnZt2m76Sp>4}+Yw{l`>n0qX zoH)Er_C8HEJOfEaG~Em+Wc;7OPQ-LqvuyM+lO%p<8)UN5Q$|AV2l#Q9owVA+D4Vl_ z4L>|_qcD_;q^6AAj!8d{R}v`(^!I*92Ce%YAelks2qIjNs>sUrxOy+R+f=KHNtplp zg35X^T$!nA%=Gr6F}wQofc8NX$e-%Seb>ylm37_+P(O7r4RT7)W7lBN?|+;*YN>oI zG@mY;CB-R4Scq*nUV+4cNOg4n5cvJ56Fmths(k#Bb|=FN%6r`c#lgpmk1hvRKcuR88+e#(-LG9dZz{D&YTQD(|sD@8HjhW}-;+*iDcs=2E|Vh}SLlY(em zAO{H){UFnqj?U_Z2wfy$_Sq*q*nO{nhYkLUEF9yHNRV89P%i0o_5Yvjh_b_?oTH=@=5wj#Owt;N{uzZq3nbD5mws`AfI~;E{-;Gj9rI0Ri*w zF$6rzuG?@jYM_P=KZv-rU)8Zb%^Wg6p0Vz!nW>c<-P_LLdkr2ulattOtJAL`hdef> zd&@sz1A-O}i8kZ2O`O&0hL5u zW>z!Qn2CwI(yi<+1m)*5E<`f&;(R$%Q}+4bl9y(llwmB)-Z6Ppxp)d zUO`Gz2#LJ{Zk?#Oxa?S&2R$4ypI1Rz@INsT!t286BzRJwTe|#ph|hXOwe1FN+ce*U zgfbA6L4u?(ah)neR`!12Kpq(AY>1VYXC(X%^htwN*YNe^;od^h=eGsQ4LFiD>=(_~ znC&<+3WLm8+NECQBhfxE9)c)@;%E>SZKakIiv@#%>UU=b){cx zW*=72x4*#$%R~IvpEMmCvOPkL8ear_#iYki!4bGw`52fNWc(nj)RP(L&QV{eK zrh?@VyC2dKL4RMQ23CJ4oPqNZ8O7jEWXeRWJ9w&J z1XkJs($3^CK-lqasOMF+^ID)_gXPh~C&&{r_wVRqWoF3X%+>=zvI?z7 zpmNFY$BN2LxiNbSHqKo?sX~f2T;L-cw$-|wjf%`g1EKu@n3?o z?EKkm3(Q}teUUUBK$qbgH^6ys^<$j3z+qru<7pA)*-EwjDF2bCcje|6P+GtN4Sd)W zbpLF@ToiFdO1sC~iTeHare+pabLw zeo26-C9+I(MPrp2#o<{qotW-(RSM(c{o3gpq+CKlvt)lO=Sz0+Ky&`fOM~BqLnMza zdbe(~w~dg+F*}zKA@TY6<7I0Fiqx)rp<8e%xHnKj zldPQS8)eur0*n?!VpN;SAf@1-kK9fF|Uf^nx*4L+*Tm8rQrzXZgZw<FvVx5W8E>KF<1ag=1;XsHlfhO26=9!*(Tg*()?Zxqgq0^#mxSbL98>suZug zTA56i| zVrdbg4HrRELj#sgJL?t>8J>gAi=T9`pDHL26*nAQ6HZU~{8=bAA+lAE@5$~yp;$|r z&||2+WELqHwtyu?X#NztZdG}|(OsM}T+CmE+)zsq$2mQo%rt+KPa-6SsGR4U->%MV zbm>j0{ll%NXkDGBFL~O-(iP!nD=&`$<7WHItmjD!V#}s0kF!{%bnUvPUA&_WDzs|g zK7bhWw$*(E-~bCK4l#=!2vg(}Ig#jpP$R}w<{FV^10TW6914Cj2LMfXhte+yL3050 z0lqoKkvM8a-`94LV0lqJZg!hhHR2cQTrzkhOfoNI@rV3G?Rl+dqg*ES|7ItAe(~sb zGgE4?Uu&!J#n~~;p-8hBblRJlnLW*pK5GB&Htsp}I5wW}mN{E2W5~GUdYj;0Leq0y z-2sqPcJ}wHq4B_;0?E!`J_3fos&Z#7bo1xvaG+Xxz@vf>*aW8RPF9O=Zhi#UBqZPC zV%&c@tYFkfS}f@D-N-+}j=EVnjF+dalPN6&%?{XS#PDOcH|^{J3;{rXEIlNSw7jo* z-WIS@_%U{-aO4C@zgRp;w|-Xi7?5iK8{1bLK^kgb4g(xqB5k&cKO`egOVg#X+GXGE zcs$)5e*1`yn1IK|GxE_Z9;QeM+B*=~-c{A;bWf|u<_IH+7G2q5e`id>mWw7cylLYI zBSOz&n;O2w;5L7O8n?j*iwTanqdciPq{VFFFh@c#1*E4!TjtT~Up7}Ku~;gvAvqPv zZmx}|pp}JAC?lxufF22bo@AmpJ2=4LIvNn%#RX$Jt4Vnxgo-I>a^ID>198>MuaS-i z;BI%Pd@i^yp}Au!M4W_KW?3ApMPC~(@5|st`XMdT5*eMx>E7Lbyc4Yh>28uteex*w z98Wy;moFuN56P&kFO zy}(5YvithOjNz5NBU(vW$q0mGDlBB{VK&x$0PHV)By`{ay7dFnCSWh{xi~5&>DnT= z(edZym;^Vmsk{X7gO(^=wYPe)!^sBo@$y29Z(?R`F7^{b&23f<57TVU-r{BUlqd2t z-7b>+!={uK{jvT>kbqS?WRZ@I6`*;Y-Wa13x-Rw!&yeBs&516)Yl>O;zv-SWx?xFF zz4<6SZ|T>3#qxK(tb;>~=m39e1Ju>8YTGquGtuW0wnS)B&HPSGM@ed4xBVTd=QYMy z=@_3Qt_0MXXsN|{%Ih<9oRC%if4w00sybKz$5ZW}s~BmA~MZi+tSn)!qBjC?b85<-;-qw8j6 z^<4j z)<|$3J264*ejVfFJBIJi=NC$^K@lYA%VPKwU3vtAiL0)&3N64hv5o5Y*3_ngaX;x@ zn7cmsp!H=dnEa8Yaidjlv!!$O6RE!RPre)vK?C%MGf_(VeNbQbOHZ*C0i;3Oi zzzX`pp3>o~)ThP783%?khRCOI`VTIpFRlBWhjwfN^rskOIeA$;}fO{aa&*VPY1DKIff6qQ`;;h`3D#nRq6K5wu z!?!>9Y33**bvm!kNSKkAC!^A{wz2U!CkGP@?7F&a?nR*1xgT)@MgZt;SGX1vB*My-$?x8A}yF>sK3KV7QEf?tD0HJ{vkplB2C0V6_&c`8OUMS zNH`61x8U)+gsMsRP>^?Dy59Sa4Z>}t<_wA6ge)BRb3ke+3|{dpn)WZ~BS4y`?cQ?J z%6kUgfSiYJv9Ot%`!Ul(Y$vDwH>RE--#2{*GZ1@h0#(O_It@uUtkrtUw-4wRV{gqu z_bPj$JkYvrqKzfgk@e##Voh#XfDl(tU>}}H(`Do`Z!kxt@BNZadT29I3$jdk?lpaF zV~(5wCgZaY{zj%r+oyi@RogfztRkAE<}4`!l;K*luISXS8|6Qn-W%u59=6C^VW-_7 zROt~ST*@7-9Fu#>V%VCCODCiHg5fG;kpuDx<<*Ix_(Zd7YS`uGw0?5yNu!td8e8VW z8(XUaCaBZ%&7GAmlHfUmtJB=}L?_-jBZ^%;RsB2BQatXyIe#8rFBhJyH}2pkIa$y! zP1j z*`OXx7ZTFr27%3dKng$iCHqF7p#2^3TR=Dy;qWH(hB4`&q(XTFk}F+PZiqe&+BBr6 z=@B<3^$=7HWSNULW=Tv0d=_qd*R5vEL=hihHA@TcsCoFPfwS!b!ehZ6YEB`3q&J!C zyrf|oBec(ihT_KH$VecB0`Uex{jKc#yTRfg_>JjA>WxVgNmt&~^InfT;v{oSsdL2$ ze8?cE<|>xtN4a}?4wBwX)m28y$5haX=HhafMoFTfdmY#WS!yuw|l^Lsc#uM{qxfS5TH)>gSSV zSjawo+FL`o^95a0aK7OBRgnVqCV5NC-8aP$TgMW5H$C~GgbebL5_V8j@%C;IN3D2B z^uj<6gsSYN2dJ0h8?H_^!Ym33TS8O>Aap?Z;ofO!4Ed|dVR}{kP`dmQ z=`3j#+O`|`4Qy}FOaADXxGY{=HIVpl1v^^r4uAU6XkcsOX8oqE$=iwJjbeIA;1(l0 zV$7=V1&hN62MRVgNf`c}e9(2e=Pv9OvG>RL@=H?4Pr^$)+_}0&21U>AjR^s%7a3%w zDP66#PA)O&x{fQAP%jDp)4s~76;YwZ|BEBF1jTbWDG(0V2>f}$QWM&S|1xiEY>Xeu zae=KzBp>h)K{7p}9dmN}`NuBFW1~hY{6S4O0E(37bxm{X-Xa5M2UUc@|BtBej_0!f z{;vpGS=pNqO4)m5?@?rrY}t{m>@9nf5sJtrdt`=?kd?jn-oNwi{(c@m|K0Z=mFv1* zuXE1xJO?em=&fu~%oQSJ%+=L9`OTly^I`2o!?J#MZ?6uw_LBr4Op9RV*0FwwaKnJy z2Y9X_b}wPT3hd^TiyV1wvUK3M^`h?Wj8Gj#`FL8vGd|sdmt!3HqalKaJO+}5Bc8VP z0@jHHd;8b!sZ+vwbCdM4**Q5Bel1|f(*X@HdolvwCC6rD_Q!524HTz=&H5ZoFMGt7 z%hAm=VJgk5AsK%|{sfvOAB^^}&a=*W2bWFvuvo^ZNIph7x-cvgw9s95|!_#suMI za?l3MjC5b@-2lpN<8;1@JP^T8mj=8$FoyGXTQJygHddM!UdJ$o=Az$h|U+TEF_C!#Mhe7b2 z#@Op2zx5USZ_y?cu(O6vkzSB2hCVtowO{0Ng^Ym6ENbjR`wqGo&Lfs%h5UP`SNL4@ zS4XnKeA7y-3>;2Y+&+h6=qVHDypFBiS@@L)*b>CV#D-wob!U(}jSSv29OjTln>N$8 z&lzyU{N7(m`v36g`SzT{A#x=^4F29{5j$i)e$3Gwe04S&%uI(k43 z!ClZj7yVuz7CF*r1@isLo5r@M2A9kTXD>EDQdcHzZ(Lxbj;rUXXlWsQ=qoEL>%llu zaHkn5#B7e4CnPwma~|lCen2G#*E@hPds?eQ+!?tJ!q&z-<4-Ls$1f zX;OG!hScf81GtdODNQ+}Drtitav`foMpyln;Tm$OHLUw!l9Pqi(fSGAW^ zP2%GUN>(pR)|l?!u6AaB*?|fCxUF)Z4YO6Q;Zwlo*a!@}u%m?lKGR`x!84%Szywa&XzuQpyy$$&zjW?n}f zUH}+Ts62npb#T8T8-yR=l>zVBer@52_`91i>P~~}5nW|zT@6&lV(%fa&fDADI086M za%K-9|8x%#i(Xmc6#<@v>t1Ws$bfp~922SQ{NXxuBnRr$2^@OxfahE;3XJggPb#e+JE(Ma}`ntT}+4(vCH8zp! zo!dx7+OKUsiz~+ITseA26-&yzHB1jxIVyL+sKI9xHr-!Z?jW!y42$>JzRYI+`|o%b zlMGO#be{!f7_a@xN%tud)>?ja`8;DuU ziP)rc^b|FfuyNC|A?QxOM7oe-Eh-IUWr);gh;#byY3;OwrVLIzzQa!lM<^LhWukPn z-NgjV-=l7NIht+Cr*EUs&M_t}?dfCU_T_y5-CsO5?|44GF15dCDdB23W#WB#p$d!@ z6u+BdIPXpQcE6?ROqyPniI)vORA^7`Dr>#U5w76p*h#h@A|>A#8y(ohP{*dUU6NGB{ zn%Ci*JHcIt0ty2IK_XI{3iz_doU2OR$1CzhnyaWeNqU z_+Bdp+fF;avaVgZ9hz`8ZU6GV!_p5DegOdzVq%y)bC~3^Eu?tKl@5%-=n2`cMZ#&d z>Ij=&HsvR}1Q9dO%mmeku42B4i}P27es04h-P1g`$TXGmic5p~1gr%EqSk2#XgsU|EY7|3` zcrsYtzJ8cJ8lv!k?VB05u43T)Ae$iSRp-2XS6|s~p~w~^PAD+FaU~s?%vDuR_JlZG zHyW8={|V{F@;ue!Aw6d$$ChrzVv|+NHV9`5eeIIBPsLWT)4+Dm>B|g8ao_^s!MMtS z7xK%NnkDHRRejIhV}J3MC(EGb=VxXySBeX1{#jjs3PA}ihPmIgvHG6L{uw`oMTb`d_r_BEOE0S1paCF0h5l zSUCVPFkHziX>6o~j4c@Gj5C1A1t$V=Te;K$coTwm)rLL|3Yt(enHbJzSnO=aWexU; z?)!Q=qp-C=0nSO&j1M=J?NEDYAj>o4(bgbFw4I%F4y{@XJdumhrz-~?{?)&X-@9G3 zj0a~s%HFbNy}OW1$Tsy(A(R6XMC~w1f{F)FQxh$z1XD2B%pcxeB}tBrpk!wBYQ#^v zJ@Lx_!tR~fg`EX#a!&V&NWd%lfYp5UpRpL|n zRTLR)oB5H4w-<;AI37QKkLDbQ619^quqaPx-=;9&us4}C1NS|Q3>g`eP|o4kHyX}U zB*rAN>&$Un1e+ICP0gSYD@BR!be6e5mLynr`X)yo2!NfjV6Kssy*eG)*c;)_)nR>7-4u5~w)< zmnJ_S?pzd0LHp(|$UB4qo9+i(F$$IyoicKAjdO?p8bVv) zt&-Khck6umeQbLr?c``k?_?#`olKkybe9D^^3Y3t(&{%twYQ&`vKmH6W2NX%=djgy zZo5^#-+xCCDZW&N;5JkZrciij#^vL9BMSrf9N@G=QGO5E#%;5Pz`bD#ZSAD;K_ml< zgLEFKj_?!_1|19Cv37UZrmz_2KvIDn^s?OvD8T;!b|k2Gh+9WM0Hmr2w z6iyeExlnQa&ztQ=>3E z2)qlafK>z3!Le#PDwy!VN=ibN99M}XL;@J1;7wEpX{HjKQ>_6QF!#*Dxd&1u5Pe|O zoR9LkpaVAbsAA@@YugkT7RFa2So;H+^l5{LQhMvA%lvD801SYAwF@mQ2iU|y62@lM%zHy#=T~X`I2)_ML zX+Q1OhUpSa3xRTM68?Rq!3Dl=Hlm?tTzmT827UAM3XR~g= z)x9kp`_`8{5l{9L!t@^{~dsWaU4idw40rK2336iH){&1Dfs0Q z=m!Mc3(5W^tfo+KYl}3C3H|&S;SN~}_IzNwi=aDhPdA)gUP;I-e#P&Y-vuvsH!wSb zKY^Ns1tZpSMPHlXJMN$dX#1aOXzT@vT)eY`^(fDwwR>Ag4a5>)lk>h+%BLd?yFV8T zf!N@TkKhx+JO|lF-un6z1Kvj*S0L`*IT%*v;N?v^xle3nZ;uny_70p>b93X&4P3Fx z$GaBxV(yeqQK6NYc8Zlbg#ueXg#E0om6L-bR)wh@_ObJ`9-8{Q-o@T*+7krmVZ!Ai zY;2ilXRjmEEONf>?FlgwJ)L#*Jq~o7H!kfOzrGv73kFQO88!ND+&L0D22`QhhV03B z&+i2lCZODUCqafTOS%_>jqUz`wOAr&1l|GM!E3cFW4rEb531V66nIqFM9z)deHq_| z6c(q%nV4Um)s2@eo~pKm1Dpjk&85ckM&2#vcsM1%){n2MN_etNK%{KE`A6U}ad@Kj zw|~=EXwP!6AL?D8P!$vxGXMb!`tOa6_U&VEj7?*D9Gjfn4c@9JSI12Q;Kc*BEVCdH z5ke6Dt7p6gfSnrRPP`2LDq<;-%44@AZ*Vr`%->r$0QV6f5ySb)%Rt?bE*XWcBbxpW z2xowGYjAxw+9{|3!0_11sNF8QDEd~`FB&ZHry=Nsug1#)011X32f7x8lxH12BA0ju zUI)U=J@TEpZ|Yxpty7aEyTL(aC6uQ|lga^J;m%MYq``EVv`mJ%-eePhnVOoi zNO#+U9uLT!ju#38H4>ARJy?Q?8$SrdcLyXO46&W@tKmvT89`hzIly>3>4#k{GPsZ_ zhwl~(tQ@F#AS40jnw-HTv|hz-riH&DLPaUoO<{ql0qP`a~>RnGhq|R*a1)-fPQSAvlx@!=bLcb<6@%V+hhgqF97xO zWWDLi8t_O&5)#NRmtc-VtQ+Z$NWT&N{=4g1yvnCNK0VI2f1zCVB!80<{%hPBvqlpW!KdBU!QG;aaLE* z^|HScMJr*0*zn!h*#qe`Rya@bAPqT7rG+=vHZMq2sChgP1MOjz{Cy}qv z{K9?82SD^jDX{k?R3#EjVUqwI6k~59+j76ab~`9>!88J{Z9rS@+~k39yR7>9 zG|;NlvFg_+L5NVgDE0dSVM)7-LJd~K-#zYnHCtYrS)QWy7NC2V1lbLc&@;_2I(*o%HNCza_D7QGw1)-Mg5p>9;6iJ9gJimWCxQ20N)@? z4af~Csi`ltaO+N%65oP_a^hS}`7>%lPmGHBeUuDguNYty`Mpq>>xzz&Q64btiQDB# zG-z-aLPB~3eV`P@Or%s8Q2E^znQZH%q9S(Mtf;^Q_}YJJi`WVB6A(W= zeeYO%5f=wUqZms}pn!71*47M2&Ikn~coRnoIHCo%PeKt#0GL4SAce~Y+F?L-focF} zYqt-|hE>`SmfzB+{0#RscQS{EpO|_XNwJ(xx}Jaz0S4~WJ}JvpdeAIDH```g9Sc|` zJ?VUd5Ec7hN<*ndlN%;lEh9Y5(J%A)E-Wd6B7Zquyk<1BMhD`qDgn%EnAq41%+x3_ zt!83CJTg>W@<{svl@?%%hifF6AZ9MU+_cf#5?r`r4@M7?Cph(`TSK!FYiMm zI!QelZeY7Vpor5i;>|ia{aEJ+rJF593U~z&h6YLW2&-&ZO@wCSXmZwHdLunwbcreY z7WT(+YdXCFSNK&|_SYIjNT-K~0AOMjmE1JaLN#X%$bK}&YH7ATZ`E$zTf*L!{eEqF~&q?UKQ5dXU+sTYMOLQK1iH- zgAC!KQ1Z8D`hYwnkevldS@@EYALk#*vH#L3NrGyCL-qY*Arm|BGNEB(*i(aA1#g4a z**U*Mx4Pbw8S-tZ%=Y^Xj*g{1l$J&|Hcqz>fbMy`+?v5Nv!MFsX0vrL{`k2X{KAW* z_=E9=JyWk6`f64YN8vvtA<7j-vHd^RC7?iT0rxs*7^k3Cv;i&K6{KzmPZq#}5lPc9 zMqbUaUCrffQ@v?(pDSi~W2@WS{5CbVG6KN}1knj5%2V1WYBBUs$zZO64CHA~52|;f z!M`dv>l+_yry~|ALuG2RM93yufL_nN=8raYlgMeEjX45_B4~t}2;>eP$;*}Loz@nm zcV9*If4!b}*KIImQqQ9g4fs*4)%H%gs{8}LwQ*{g0m{S?uWVmZB-k;4r4`gvFye${ zY`}K8$rNNSCWeDf6PvU>cO?gg#>tEfya)sCBj7&-+IMzfnL*D5_xg_*MHts;-p==A{kqb zw03_?Y)+=vq1qi#Bhx(2NepbfH$iiq^uv9YA!LW)OzcATUk zFX;uU<6a4~ynm9W!k0NOID?6hB?#Lj|v$c)#iFq$6Sf!}%O# z9YDNt>OEH`uwkRP{5Fz~@lO(0!{3N`4X4>OqDn>Qd=r;$~oHCKTQasgk?3T!nEUiPA)^~95tg&Y0CAr=ZPw(sgOk-khD zqlf0h?TmM;xHK!HWJa@Vh%foAxI;!dO;a+vnLv>2DZ|xOxAC0A&FB-YU6;+5G*3+ z^(`8ceP3Z9ts)FiJLd+G3e9A>>bs6Dz)8rd5!v z72Ib64ak z?+Cccw-wB9Za#cTnHnAaJrFBZMlR(;PX^eHAbv7g0E{ct_%es+fPwi8xD2?G0#JY; zAHb-tYlkmVJ?T!n^~$f@RX4A$Q_@9=(wEvEfJ1IIsC6o9ou{go<{o$d)#pZ47Qg5xBPG;c;?A zip1?OgHp@v*}(E5k&@Gl939H#iF?3Jf7+*82cQ{7fW1bBUFv+EeL$xK#1d$y*-Ye? zzjmvyRpl*!0N_y_FUZO91Fr|bzp)OT1y4nBt4B=qT#ydK96ru@ zRM6d%@DH!URU3_98GK9`Rqk=Dlg9hRF#K$Ic6vl+VM%9i$3l(=zeRO@We=Uw9!AYu^ z&795J3vq=A<9q0~!ThhHs%md<-J|xp_#?iHKL3}%_yst>Sn#`9GKY+#`HwTKK%8J zC~|@!Rm;%O@J54vW*5)J@cQOIEt1+MK_)zy?E(({ly=Rf z;tz`*n(bra@vpP`fU>T(&-A>2FBVd0>cm-fXhv~W3oJCb9U{B;Ql2<*#>NqQ$xwVJHvLWNfj+}G-l zIV_4=UfmOY=ZY!nHe)F1ajY8$U=cKEJ&%m z@CZB{kO{0lvlna9FPPFKh2rdGAnUEwEWLSM0j>gt07I*Ga*<-mCl(ubglpkjGFV!! z7HL;z!1QeMgD;!Oal65QTEN5DQRu%V@H1OV*ozT{X=k}-*O*F@i&{De>rDv(B~U^Y zyq2PH5Gn=PXe2w;|8W6gU_%csEE->Q6=M42vPC*lqbPkf-g^n{? zS24!ICp8frNBTd2JnjuZ4Xhhs*2T{loE5BtS+MJ))A^%5c~>DEWC{}@|M&G4y8H9H z#NffhP+8g832VaG&a=bg@H(Q&#WL4jxwA9fib}31e(#;XsDOMvwj5ee?fk!w?%A{A zG#%~sMHBPg%Fd=P<*P-amBXj;qysi=k2jJzNMBzJNBO*!qI3Lcijf~jv|A}Xx@+L$ ziJ?k>{{;&;N^X|xGza`x9C?HWM`u)&u{aeZT@JziSzDZ#-)JZUYLIu6!pZmPI-jC_ zd*-f7Q}Kt8{y}J6Q`p|z>%Fof))#4VE>bi)Gki+ciFXTSJHOoyc&6M-qS=_t%Cd3N z9PrSI8?1Gqc_@$@2WLV~QXpqA7-!#?HQqW<0R305Y}v|52a);kCi*VV_0b*vhGf{n zLoY!YW$r&liS2O+0DNwG*%6G(JsyUM_PC922LoG0`u3s2x-mtu}0=H)1JSTH={k48LoUv9}gKB zI|SHx=+T>M0Hu9pZ7sX9G9E}N9nkH z3)y%^5n~~pKRgI+aKRZL>m0DZV1Hnq`uRMW%cjG>UP=?GMf(n46pcj((cS5xp)OFd zD$(OnI_u^Pg2NJNo?>B&3*Xv!!+QlWJ{z7nE*k4Pf z;&5K${69!9otr|-j;^~1Tuq@(wpb_m%h)v5^7;E>k)9To=*-)wmzA5G6=1PA%RYBD zfrE^zg&K;iCL#778Amk4k30-N?CgE3QmzUke-5t4k(78pcLLiFIz|pRak;(DId-ZH z!ALrDFz&sRKMa#P28_D?7hV@LKDB8>z9w7RLIL~VvmB>C>elh58VJvbg5b-9{l3Jc zJ@3bvZN`6lkt7bTgvgl9()`JyRNTtP!JU=x(Pq%H6r)C>YxR9^3aVt$h1^kzETBwHkcMqsMlK z>6orXuIfG=$_A&wq6e5{i61}Ub~3U{;g>{PgfBI|(W>~2lsN$RKZd2P3nEPC=V+(M z^@t6@!q_8O69}3fXvxvr;2)DwQ^SL;g@Yo*a{=%A0r$IYVaG`6HK{>|Y+RbJ4FQ&Zl3HRHkRKW<2jmN0?~AU2dDumI?seZZ8z`>rNci-R zcu-IPrv5RsTa)J=l7k4MikuueL+qWz-tWKmtTpwv`<=sfTyNC~iUG?Bu$aP;6btr8 zp-(TDU^dq%jPR3A0dslgY$gVD_gqVqo!ZwDPxWN7l#HYGkt1+(zT6V~nZ<msV8Us45oXNU}{BnpfNz(ogI27O5A9R1a7 z9fph(F+-Keq-4#CQlM^oeWzF=ckv+#${B=NaoE1OPI7`LU^JM$xO})%rv}P$edgYT zFCB|(AM5Ize(idlyToQ>NQ#LK-qSAlzj~R?{^Q|;xj>MaTfAm4Tw$oHt0OBJZA;Ty zFZo3AJzyMdianbNL3b-C_y8O0>WTCnZmh)kC<$74;EX`6nA>$FCMP%fIsbiT{ysTr z^XGNb@8lf$Iv*;l@2XmhshSY*`O?ZW$%Vf?Cun$h8@b*s*E}pP4hRnMc~^(^4s-dK zC_2d+^|uP$JWmzi1Nz-U@$AP6)#v(PqUs6dr+%>}*8V;xM_wj*dX1wpg}5gz_*|^X zI?YWY!@=2@hhM;`kM(o^-|a>|)J?86k_Tw5C#_1cW^^VZn7-b+>&&5!syC}&ui8A* zRMr(~ne{r}oQN0|=z{vFySEE;C@iwA+1h1q2(KV^zJ&a$_#mn)bna9yS_O@NC@$c* zi-?{g`5OZ>b0E6Wf4;#CIgtkyan@GSda?Mi4^d@fz;!i^&j|&>D|yKoAQAyP2oO5h zei29_vfBSM?1YixX0ia0sr~Yrnw8nZ_E$sm8zd&1MAstxdV~_`6so_FZplq{u8lWz z84H}Ro8jLnGWzg%RnBfzzF;EZ5H! zMY1;FHEI?6mp@U|7B|_Dki1>CkJJpTmrE#{q7DewO&331uHt?_#Rex~psE5z7nT%V z-4$6Qy88NarVfKPFkoS!Jt%x+sU?O35tc}RYGdx6tk~cwrSmJ9nsV`{Z^itk472BL zC$pUe7?>zQQmeD=} z?09Yeu}#N&#(tm@kB&+^7;SN}iEZyoi;{^Kj?l{DAt50_aue|EfIHUUaSYDEn1IMR{*1o-S%9y1W)j#6a1Ix^mWpp7hc zbIxB}RVh7`-Bga>*AQ9o;25o8`j|d-;H7`#n*xhw)ASK71(!ymlX20`H z6QiP{Mn;L7wpZ0Vb1&PbLea?Q!(~?p37cYVVkX+txImPuHz1{N6_x&Z4 z1)EUjj;`5{BuP6v9!6zK3g?R-$?7I%ODcLo$7RO=#?nJ`i&C1zId_`%vvbsKC9Cu6 zL@b-z)aTE9Kyt8O`&IhnNCwY90}$!*2)Tg}?-M)POC){4X!@l`X-Jq;)KvTjxQ(%j&lJg4 zKglX{IbA5Q#n8iy2>$YrT4>as2K+=ykBukZ2RT$gf(`AnB6YHpe3wv6U1cHwvCrXeRpUyw!iy2pJ+2o2^|UDLDJ|= znaoKR0pZ1CGU?E_Org2C!4P>*JvD_lwK*>6y*)o>({aaA%bo^VENSN!RidOCmiSTw zwSt22bm%>6;F*2Q@8nxML8YpfmXczqt-YOCQOw%AF?EhD9p%lwL|!=JB5Qs10K)CQ zFRryLu8}xbD-T#D zIlbZER_Jya|7xkzTTZ_RYp<`UXBk^k6#}>+c8eAb;b;PV9of9_F0(_g*XQms1>M1L zSJj}W1Xw6YLBf0;lUlFQ6!jQj2nun)Zj^;w0*#NPU*{-9Ye2 zXxi360NX`^h3DCXJvkZ>QZR%2QPsnw%;0{{d4ws5P1ja}I7ui04ppKdvTr{#lvDe*VuTMRm!v&>m+AWHm)$^jDX!K7U$j zDZhnH94axyP`-J>nW1|$c*vo)=!CG71kw%+tA+)?zk}OLJJ>vwl$&Zs>1?ShCJeBB zga{K@S|MstRIg@z5dHVZTI6&YP%Z&EH)Nl%u&~OnKPcyUEL|3lx~1RhBeGA(t%Cc3)}4x!V+w3B9M)VHodK*7I##vKNJ(z@g}QZ;9Y6Z87kch z`)_q!l~8(FKsym;<_Blr>l= z-#T}rN+_a2doqDX4BW>%H&H(fudZxw=Y!aKauP#8M7Sh)bmFZwD3r7E>o7nNKCqkv zFs8D|WgPer0I9yKpY%Gl>%}8&RRV{)^Jr5NNmW2?K#c_SI8op(l%AaORFRE7G8}JR zMP$Y-bm0O^po|rE-0=4-1J@1U$JVSHvKsSloufR*dlyye^^N&tEpcEY6`~^7ppob3 zc;(g^E;$f{!F32-dL*C1oFH}cz|{~Q)_k0HmSZ|obaKx@RBR2rQr7MfK9i< zoVvc-;`o!>Y27AsTXj?^k8koQczff5IGN0;9?U2jJZ6%A3O%>gL-BRL+VztZteL-4 z#iRM@mXhQmirMlzb>Le$09FDl9%)zug7sR3So9FG{cb_aLAc-X)@oa`9sdu(zsC})HNZ4m4sr>6Rvn{UL* zt9@nE>X%EP5p%An3d^&_3i!wYR}=U&z?uVu*@#3cXB>3tQmu~B+32=^Or!9<};H8^1DBYf(lwMTh=2o4;}S)7s=vfo`Ek-_({Tjh=>HYI3{wkZLh>`&p!%%Eg+cCO5c9Q{qn$jBJg(-TdT4o(-)6+lSVAV%(u z+Est{#~vpwv4w2RWyTJ{T+VL`NJ5p@_;fyqC*VKCqz!HTkd323Ac%#hr-#?_ zL7bZUWnUe?o6`6vc8}vNqoWPJl|y$}I)L&B$i1;vSaj$_XnyIz14t1hWt8HN6P25d z6Fb&M$Q@QK;kBcEDMD5tEG;ou-oO%He?*w!>G%W!k+l+%x$G#K zPX9hue#Gb0pjgie-+w_-k;26!>^UL5tmgXayd>v&H3|elBjhHC2w?717~R0sCptY{ zQTtCHSze*r8W0Y?O-t#jFQ9#RpRi{5y)0m|${2V3_apP&)>>k$GkI;RXJW69_*4lB z(zF#czC|Pvw6BK&2PLy0DK=fbSzvoJtDYX?=E;h^ke_z|d>()>z##}yG`T9mQ;R)h zHBjl8svZ#0Nn79$`FV6~B|E^}`j5pD^xfD9C_xsS{I9>2De#i#ef*NxN=}x%DoYm$ z!R7N!lj2}ar<8p>`SUs_RJ=O-XC4#gjZJ1?TdZx3_Bu zT;X58A_vhq$K^aP_V4K6KRXk~P##)+4Z*jiM&&nD?U4Sw8WiV#w;%c$Gk4c~O6I#Kh6Ams>u!VX-m8ZufBMHFZ#u<7Omlc>umr+~bnW(w(W`ZpCTk&fVft*-W%2Q9U zj5c*A2>6gqO=PiqI){nbo}^ku9^|$08&$I>FP*STq7n_R=ya+CMXV&{Z-jmCX*l+dUL7th z$@{cCYoA+Y)_1Dd7#m9g?$0@KapC#}(vpkfELQv>!}|^^fG3Sk;d4ncSG$i@rE4HPAi2B=TNsr2KHyHS#bUyz0R25o%NZ za5p$V-L$Z@to`ialL5s$s*{6;&mRI->^$emfFZ-s{u&4};ESkPe-zR4kVwhR)?{ii z;ad|qb3Q@YbPN`hB&w!*c|1s}ARC`+F-_0;So9~6i9=twh?eAs z#!s;ckvGp!?_bbBehUi_ea#!5*nlJ$UIJ)dRLAJavF|>AwmSxj zusMaE3xqxj>HLSEH%{I1!H*m&0;oAbtIQP2PBUtCGW`t|ih8t2TBQcB9ky0oSg8s|xj zTu#EE==(?eJ-vw}O)e90o5S?U0Gj%hx~d1(xOq1zA~==k0C{`jFf}8o^%6$zjwP(4IiuJbE=yeZrHI5oO?h zSKz}-rX!7;^-S$V$pzyU-;GE9z9IXXbU5FHGcP);J>o+iV#qAj<#~g1wDTnwrN);( zqCQRQz8O5k=!#mkdoFnjwNFBYL0wPrW->pNC!S>~HiKNebocoL=pt)NcUQ_*_D>5w z_drN996;FF)J(~yIpjGR?lWF2Zc2B_M&AhT<|oxL%{9B7CHaozO)CnA0=tmFy1lK1t1xfcv>PmWF?qCAAFITVDaKs@A~^_apuI6hWa zQL&%imyRT_HO4jPP6fYAL_V#nAjnX1;pzZx7);i-pEDdj2a}=Vuj=4Q-jps&7uix- zQKgiy<}~hq`;|vYkY7}gUq+Xdno%G=i9jl65Nt;wFBLY8Fjl~&4(r1cEiga?3azQl zeOX!<4}k`JYXE3T@yYDA0?zzcGuNlF*CHOv6jUQ_OXu6kpMjmRrdQy5LzW=KsyT1W zFeRd;RB>|dS1Rv!m$LB&2ivu+CEFP1G9D$D*dqCQC34+n?!E)F6iYm69Pxrt)zrZQ z$NpL$w1^hcckiXdn>|G`PsdW;ED3E#2XZVQ3lHpj@Yb7Addy5o%S+N(#mD42Jm@N2 zEOLDjG5la~aw8|Jv2)3Q$mLQ-xx{0s09Gip=Ctio0~T!eMyx7fX&30ce&sv5zIyD; zqUjy^6*{8}BzR(X+%hs%phuc~>ZM_YWq!@avan~;ZW*}Ti zAm0!;pIuN7U>k?es04-uZa`CiLK>9cFeHPA=Rg|%g9P}#bG~DlBAk~ zzzXSXtad}NK8i2a3pzd{PEx&#MaBPd0f5^M2G~CSieKVLM7RTdckyeVz0BS)e#oyE z^(lB@8RUnM4V9dn{3DGIziz5OWKY95yNI5V7=>Qp_RXynj_A52g`Lri?yjPVGnAOu zl<{(+UjmcRs1}#klC|SV>J5H9_MbcZ@{WKLZTBv%!~*K^+cue3ugK*SicE&{j?cXM zLhyrRl?Tx7Z&%Z5xWHbOd&IzQj>}(C z&4@3XBj(7zD}&eHYZDbG8b?h%q~10|+xtr%kI-FC^w0BKe3qomWC!~=$Ss*zBa@Q~ z#>R8OyrMIqOa$m4+vD>(tGK=b{&oUnzQDF$#oO*b!(w(04qzL|s417sf^GxMJ%Imr zayP^0$_+Dc)-sGs&0k^?t^7JxO6CGx?jCVCj^*1tDHY4-#B+?fxZnJdbhkK|4B0Pq`;##)9+lY7 zX?KBhKE8RTM;YHk@TG4>j*g7jFLZgE4K$kjYZB77+tdD>#cUc6L&K$&z`@Z?&u!0x zz5pf!M22Xk_9cn*KjEPw*nERF^J@7~jc)j%>&kmmel`)j+wVdWZ@;~_QZqU58zWqu zDY)x5Zr;{Jo_Rr6*^gSM<+qgPJ;1OA-)%qdw22TH`*y(x2jHOF2gp=FelfUQ(+m=< zhW;MxJwT_2OGqdM`#WIP@umyeu*AvT&t}?7Y`O?Q$YnGes;jDgXI|z3*HT7?n#Sm0 znkrCYlA5Hbd=b@ylj%ACdm09GPSW@7y?5`J042Tw%{TPyH(H13H`k-EUZ$k?A^Q1} zd5pBb@pYovN^VmcIPn~_CG}(}_rJ8-mMVN69ZlNX$;gt#=ftqOD#SohzWiveIV0jn z57D3%3a}L8g(5k6CuB6x60#i+v6Xd_;Nl2pB@F-qP^Z<%hiA-i%z!)v{kK{9 zxWT9$m|NKYeJeuOZO*|VBrk7O9258I{+q=D6&d#Js~_PV%-J{M-m2laKKsI;{3-WK zM_cDQ*JJ~O>pGESG-Nu0!VZXkpr}9)ZuQ+)KmMgy;HGC|i#pw3ep&kB>%=B#a_8pe zd}+P!{?i@7DFz!__^hwGZ0%n2Jb8kvl>jOPdLyGu6fSV{dnzRrc)h&0=ZGvuEiCNa zwN8pHPHUUAqCBlq@4mR6O_b|?jo5+jN;IyQ2E83m*+v^3X_B^~iPYdpq1nbj z5;twd5QxU3|zRN&W77P5N~bkwLUl`W&@DUrD}IZDUF7$Wp! zkiOa_JBx9kzAiULo=Cf^N0E3puZhPW6Aj%z={63ZhpWva=^J7=Fv6mqoX;3^8b3kx z?2DvG4G9UM}d3C@t_)BF4@>tlU0tj z6K$0~7Vhh;d3`ftuLo<4^0+5=rgNufy0U3^T$_Fraj_u_GrL%SqEMNwY-qH z(m~hBq>HC5zVI7?vIR*T6UrCd6~d-3)n3}yD5mjxhg%MtgQytb;aUt$nB@69Rs~Tc zh33n1iUksXD*A*EtvC?E;h#A&G6FaW zFjR#z2exZ2f9j&%9dOoj+B%@861j>-NmPy+$!cAX;oZ^W!1!iL;M@EnKCsD7;l5)y zAYj%5&JSI-rz5}>6=ZhcrQbH&3=BXbqM$yrmk>OJFoX5WxKxH@znl*E2z#j}6=?>X>v zw+Rasr!S@0(r4ZjqoMAl4iT1!!YV1e9hGx4vl?I4F*+lc-dX|g>0#%O-pJ(_t8V?7 zQHFEvl>69V_U-8@SXx@TF;z~M^*N)y=v_eAy1W&W@Czw?AywW8>h~X2-oFza8gz6K z+1*y~+P@v6?#HqaIGLuG8vgz(%CqW5xv`S@z$$f$X@xL655qOP9!YJNqo}Aybr@lN5#vj9YURmge_SFpD-26f zXKo3#|CEV}TIf{nH(EV-6_>+5mCM*d&oL+?W+td5D>XDvCmX?mLqsB#BozP_otuY) zn>#-@cT{G6D_X{f3_oY+A9bddYg?wK__Vio6jqr0eggkf41EZsRe;flq_s5*kn=Oe zAIRdtu>eV(f6os(CkiBCpY%Ur{VTWWUR+nd8=`XI=*RiqdA;sq6cIW%^7Q;dGr!Q0z0 zC&#^VYn>z~6^$1?Z+rMS;YA3O1<0Y6`7b#A-FKODuKep%O?$c_Gi^($LsD;laUg5C z!T&mGcb@UU;VL9DWHEN05ruw^T#2oU=&DPiI??qD;hPu4r?xz*W#QDa{&#NGMr;=O z%-(A}swPr-Al;dmj>C74#^nwglC|s9v~8V6dlsLxF@Y(ss#fF=uVlKn3@eA z8k#Q4O-( z{r1C^qEaRszuTOVa*Yue^8^)wb!&@KA%-_&5 zqAF#gj4oCnYf0T^iqy2vt4MgjGhV8uQb6tUM7(|BX47(OfD;5o2E@g2YJ71&NGMxr zI^XiJwBs2ZdqHJ?u`_$w-pL5jEF7&Y7`Sss_Z4#H!j6^aSmYUF$#bZNbmam>;o6j} z4XHml9pB(pZKLAOSg>$&dapq?CqjaRh~G+wpik6szQeHPA2sC{+H+9 z10`V|t#y}+ri}@)>WOx&cVCw|T2Up^v^oj1+ zFkK03wdO< z1kwah6M$fw%>*%Jk!`a(b!JDxCRzX${Gzf%ci@6^2HbA|QenOX8z=(w+uIGtl)!D4 z$f?~`MAJ$bhmjr_QCbPIN=kV}ML($SZ+Zc=@B06EdJCwo((Qj(K@de6rB%A6LsFFP z5CtSfKmqCQ5Gm=F5|EbeZV>4P=?3X;c=vbi@BgkfGwZH9GtLa>oafp5lMcKmGtMk9 zuefYmCsR^TAgxJ7OcxuVL;fyYjE^DD^6)%GYsfc(U!fJlo_N{TCgo}4j+;LQ&;<419b$(`eExYi!NvIV09Zd zxf%I3^PeT1dJ7&R|1go{cjRbHanWtzpVT*>WiLyZyT}^ozwHmfOLf0q+z8C4pVO$` zjIG|h-9bDlK@*U3Pcek&HQad|GWV<8ow#+}J~7fhE5BnfLur(wJ~Ryc8ssHdz)b~_ zEr|NFJ+6?!>4Ak1FHt&1M?0}Oj7}0U>^T#3#COlzGc1iCIoSbj z22(GTv+kqwdibCd6kE(U>n86PTCVz6D!s5x^hf5`USVW0{h?0&ROYD+Zg}4x`(*$5 zcRJdyV&9PUd+wJ^`L%X+O3u}VU?PHl|8t{3z^qSIS{&_@ZnAz-%nE{JrnGQ_T?ePf zRv~y9HdSwB;+YnaNvAy<#>J7y1xU!H`4%uG3Q*8g zpl5zy2R!zS*_5cW~bgcw$H)go_{4yMp+)Z6w%PZ zyqsi4+u))ieHU(OU;CnoA#MhS*`00a#~K2jSmlZDceb5sh5Tzb9Y*tGB~44+UZg5% z#mXJr^_5IF%FHL2HMsqjQ7ZNWQ4Lds3NP>7-zf=IPtmZSBa@$-nNck{%{Ar=A?3#; z^7G)4kSN$3t}&ZkF-b{fPr07dZBCRh&p9~P_)phB2~{UDWoHIH@_Gn3>WFsE_TrV17HYzR?vS%)^i6(nj|40 zj|PilXQwAewi9JSDqS%MJKc3E;Z%p|$$P|jIr-)+83aw0c{iBkI6PjQ|TJmJ!a|Bs0XvyIrf+86t3q$Lz^A@M^Rp2}5jG%?cud16mIP9UG z6jTEN@M^HLhwtuMEG#aQ(V;YsY=!&4(?-7MXiQet*1`Td39IEBo1}1&Uv;rr6r#y#UX@(BkRl; z+*ltrMzOMTa{)`mfe-urLgT+n(BQn3kRZv+^OVEEBpsL2)TH?IN#Cn#3BGC>xFvTQ zY-hPpG;ITY2y`Ypw!^DQl4j?Ri`UnvxykBUW4}n&7g;eVF#W;uuZs5&&->uwWSYF% z_qyVYGCR$O$3Cj)-~-0pUhW5yrLK=+Iy%03KQt9k?zF~|TXas2$Jw<#k(D-TT4LLz zuiNW8F5696NN?EE`}~RwcKqI6dHBioU@YS+RG=KGO>^^jO>8RoKySTZ215+xXE=VP z!htz#+&E&l+f>U?NS22NG#JjPnSN!8rU4AUn(UqgNC6z+(&-RG^^iNga98uvh`B3(V|BF?mwo>cQmANqH=22N9L!0Kq~mZ+32O8pwlB zPaXcVtaK#*{X0D!L>G`C3yZxukizR1c&dxnDn?A)w~IzQjn?dOUo%CWBKbAy#;psla|xFU51oPt19~ zhgK9QLa5>TR-vHGz~xYBF0oxEh5>{}=`8?zgA8QmDKgd3N7}Ap?@nlI-(=^tfCZdz2 z-DzXu|M0lJ6{{+>3Ntc)e^(QV+Ys352=ycrMf!K)xilG;;Nakh09y;#sB8jn7~F^K z?ChYNCxJYG&WO9);aBQTCdXz+#QYGS4rSa>M6>h4G2L$ia^Ya39a2_E9%D2)s=VjY@0Sm^!*#LFEV_KB43t2As?{j048v z1p}>7NLC{e2XQX;Lj1EAu`B2pS&`C%=z~?}M_N?WglOgc59qW}P`vh@v7)k&v9g#` zK($k3Bq=&CxVN+y7I1(6Ce*B(4T89#_I#~(duRE<4>adVuh5O23Bbd1Db zXX~i-bvZX^QhsxI*<+|KGxOW=*_JhX7g!3#R(8*P;#{3OW?-om+0@q|xaUc1@T~0d zlkhdMl+nF1zks>NcVffTJIw7=-+v1b!M+1&EAp8K4D(ROMbE@UMNU5d-I^Z%J_v}K z!>QjIDS{R<5ejHnp#~LReqbhCAO@T+N*cOUvBiKRxcecEB_OrIKIthMOu_BJWPt-8 zNe6f44a4H&H_RCaOmv4PTk^){AYc$o2v9{15r?47y>yQOID4?a-=oLy-~<<{fMDoN zf-qrz=|&NN=m>F~$mDF(2_J~VzaT_LL!LZ@%FXvRy~*Z$J;EIk&sF%k>Idr=bjvs5 zq&lv8Bg>JUjK&+{6iAs5e8uz31h>6v^pcw?vzBlJ|5D|HQS6Gyq9Aw%EXILQRF12bC10*jRg; zVH7&-y@zcaQhp4ZzuXfQ7iTx@CIlMtH*|PF^8iry;5P-UxLDvLl&KdF#lk?{hj>Ug zU-Krn454}uf}2*Ntz+_Ya-h5p5oZYVNlzH1;8p?*n0L9IP!gxjKqd-GNhm)8k1%`! zG0(J`O}TYcg;%Yr6zT9mh^DNm=@QXr^#L)0ez?|htUVvIx2#0@hKl^>J26wHf_`8C z1qG$|Jv{W&o+muT3p4agb@n!Kau4jC>qtcUGhR%wD`F}jw=2NiL|i!b9WoSKux6n! zH2G!v{cSNQsVp?;LM0|f%FG3x2sjkqPeWz`=_OeoXXwrm;sAgWH|0 z3?UHhLz-K*>O2ET{@BJD#KgRkm+uV?-fKk0g2=SzHfz}68;5`qXx0sY)sCC{5sVv9 zFE(DJPs(ri4Xz(V!xIEca=%mfldqgjMM=4s^Nl2=!~12*-0pl!MWmn`sQ8Tfk`O~E z1ou@}S6_b4kCaZoAr*X#1&Ol;(BuGDPy{rw=vF6O^8o`5xc7f)T3hBDAc;f_n`RAl zldg9bg7GZ9p7zrc=zifIrIHd4$Ny=5;rEiT)D8^{j2g+N4d6Z&5@_ZX^?Mh$BQSiky)9fxg!yejEq?X zPWSlwGG5ejw_~?co_t%q{xl)`Cm8FakIq1UXMVUwIceU4nwTzLrb&_RKH=a(LH<4Y zAPh~)1VDEpPF8g6dmDC`8`9ehHn`4bwQ|qS@;nsqe-rZfo<`nlZELdx;cluA`*xgQ zbVog+go_|@5k3@zAqY?z*x3L)Z*FdejR9UqV9Vgr0sETUQLauHWTHXq&+k8f?(-%O zt^Z6-MG9}|NV7WXtqN-yN+;VJS&oXpI=ns>jBL)qHuuPLGES~G}C z*Jjz{x7!uhr^Jb%J%|^s?fM3RW6IAp7Ib(>s9$!w^oJxA%1KLurz{=P#^~ox&kl?v zWn~Tbmr{Ip`jVI-5o+r;3gruFY2vPE!7tjvrGb$ec`KfGg*x7u*M03;J`{dJM%Dt( z1V8_KrYvL>t%-jk6TWzqiQKx7A3NBa_==-FFsIII@L<-}0kuuPK!+#wHQG{!ctkvL z&mubQ;9rAPVV=Z=sF2Is*d*;RzXD%N({*&A@9sxmt2_1M%Go9=Tt$KL)X6R4-D z@jyq1EozH4=WIRR7vlQ3=8gnPeulQSd738(cWwG-cPBYqMUG-&y?tNhBbERv5!ccQ zyAARATRE~gqHU|^Vjg3Qb2n}+KHO~FA7NhwMa*ud@YTI^W&eSXRLsmlfKj_&O-Ie1 zLAn-PERfR!xxP^4Y+?e9IMAmE{Z>CRGQI-p0zLEu!?xOi0J4F38^mLPsL9AsK6|!X z=hZU*qppq&Anx~o#By-~oqvR50awNe&w*Xdbk^$}kM)$cwzk^q^9_LghM<>CIlGnt z*QiJh;<({9H0;APZMWvf!?kSbe260yGUiLm)js_Hv;b$bH-VXx{7!cgG%$po_)+|3 zXApU+icQC7{WXt|Ogk+MSwfz}y?1WyKW`<#^1xc|H;^R zZgV2sV-eC5sh&Oa93E~OS%D&kB*95KM#ilZE)*1%SYda|epv|$${;4K^QSuv4VdcD zoxP8{7TRFYK_=MuMYE(mSQET5C%sx_?%yS9%o^LzEnu4SoT&0nqGZ$PQ%FkaS|yNO znuxKt$f!2w6ju8{L+rSC(M1&V$hu`mOEaMjfN;luG4aoBf+8ES$7qI+YCD*5e(8tUKUG0UL*n8`24Cb27T-EdV-!#f*lD z>D#}5#-P9XWNP{odQ{+agl0YfJEg*bYlW5wcDKt@J+;p|klu0M$4e?gXiMRulQtPN zH2Lf}pUlmTVCjS4s`DUm5Jz=(c7g%&7{_83qK7W&Bgb%i@7=rC{hbTZafMAp!AR)Z?!LC)Mt3)N>$~dw^JYp!a=k9D)ZxA)zL-1SPF0-YR}=4=s;Y_Eq3(4X z)L*OrKC{=m-?#zV4KFqb2mmmX)zc#fte1@64sJemNa*w9#^Q;S1rS*CNE5yce!I;d zAesSU>4UoA&Gi+;*}#qo6-)5pfo+$WnHio1blVjKG)_zAenDt5U#qI_!2^Ss6R6|5 zK5?$oMpO@$^a+s+AJnT>E{aa|u&=Q`VQgzKUpNqXh0L3H{Z~LL+H!Engn7U)ZDhTb z=TE>NiG<%2`0a61rZNxHl^v_b{^s(M!tOh<#m#L#(+G0?Cl;mMs|Uqq(#*F*=G}?q-dHxH@fA#*1v_0+W6t%xu-hF%eA+>DL1vlFKR*iYTz!0~kBgN2ie>8& zEmz*7zf72^l+BVbSuoNETz8)}b240SR8hN}rRSOQYm=28(TH!TpLpLzy3bocH!!jF zbLij8^w*)-4`*np4@yif-DlNWFL&yErfn}OP|#?UaP$ORbwnDADvnXP+pASs)rYf! z@UAG*fS#*q$I@~72=Yk~y^DoY!CWjH@S$Q7d_Ebuxm~mIW`82-YoR2twOu>WHZnAn z^+Q@zQ$HhL0jNuchKEJf)Cl4220a3R97=TfYc|!n#@A3xr>Ca}2nV8O2BNqeug_M^ z$B$HUNA@RYucN_ov3I^vuz2KjB_G`44Dg$CR`jn-L?R(TDZxOM)SCM zcnaNb-HNJ0Y7F5cFj)y&Bw-Bnd$1FKFXauxX-mbUlEXIptdDD? z`%Wk#bM}2RGFF?Jv(Dj62Tdt~ro0YeA${D3>jT_+vP{V}&H_61?zqIZvlyj^r;^w6 z)*JuwcU=j7rwdr z*Z?Skao2mc<}+2csRB7SH5Ar-nJ^U(^4a^et;eqokP>jUvk4l}1`YHQ<~I2i^QZqt zaxc&-KV($ej7-l>V`AFq&#VB$VT3={Nwf0dXQtXLwZ8d#SV(b%GyBHZv%va zrZO_}^7_3AD6k@N%$=?e(E^QcxX|i$ZI-Mz_>%&%T%Ox+m)=w@ET{uKauZxn7sL%_ zA%ChM)ARmVHua^YZzKcyU)N;ZztZ$ayy5!x7o#?ug>vicC^QQ z?8)ku>+X6Jt%u*s+_#4>EZzu7hU{N2#Zjx&(t zyCpnW`^WWGGuQLpRH8m7jy;QkH2db}PpUG!vpT1ES}52BEF$|>4izW)NJmt7lJDui zKdWHrGZNRH)h-iKTHtpdC&ITMju&~AnN(cvKm3;_9DnODll@V~J4Jfmoeb%;fx`D~ z_rXN?6c?1P7Mn;(p+dUgc=OmUq zTwI8B^r7{`z@Zt50!xHAJ&uDm@;1KgONwpcI~+&sM%PS216gtFd$=@cXdYOF+KQY<_=2OPg4tsOpup>77|^F6KG4%#pU$>t$pHlDjK26IiINi%8yC?J9Q9Wx$n(ymg{u0yBW}v?1-k0ugzukYj$Uv0x*P^doO6EQ zS>ybb5Z5(%(dAuUKZ#N%Ev2o&8Bv0##>N;^gFd9ha6WT%D(YWw)3D`YHQzqTGiCaL zD46rnP^zQX?{2lhDKmhKAlQJFP?8PvhIqT-#tpue-}SB+g=$Pt68;no{s6!$Ev;|z z^0eWB zU|oyYUMCZq0w(lDQeJ9qHzN1>b!J0Ymg0{uNm=nsLkwLuTH6F~S-RE?AMQxweIUx_yzOxzgAZhFD&B0;LiuyDi6Nr>a#$X zYx@V|l)C9*BUyY`zYf3`p3AYk@h|6=wC3x}fhcLXjTZ+cx-3pYhi z-Q6ypCv-1Kp4KTw&&#lUCM)!SYVN(gLl&NrleZ=X()OFnleqzoCVf>uIPdbrk-6;6 zRye%eN`vwl!3pG)yFv)*h2)q_ukXZHLG zGdOzI2?=vf(v^I)(nyM36lzr)xaD$3aPjb3t^@Jn6t{JIPv<8i!Se;3Fl>Ok0zEwe z`RNca2v(~?;}HhvI_EdOsK4jch^HXX1Sej0K>;lk+sxi?gw~msC)c6Be@lWm>k4UP z_27)s3N#C#--45kh{&_Y>g9u^`K?$NM)q$y-Hh&spQU%=JLHo?T2g;2Dg6}1&yJ${ zYY<2L!fjW`?I!ugmu``4uv9Ty!tb5sE2R9?1yb{dt9Dq#N&xZ!vV{iPUJNiFPYo4# z{?=8@9)|Loma72H+>z$dJd$(#J14$P*hIJ%pd5S|{vK&8-HujMcftDPPWcme zBloi()5OenJi?9#x{XDp4#=3$=VY#HEoVp1gJk6KF%#|@*kWNKhB_`WGn8Y zBxks+M#|4_X%D%!@^G#{sd)FM(N;}Wb3c!qNmjzxm;q){ztlB%cx&I}E-vSFR@OF6 zZ(cFgy}3%IOC2#rWHOU*o6fCgCOn`^+1u(DLZ(J~sOM>JQ$yBb^o8~HHshbx9~;gpbkgh%rx)|&Jy&q? zCgk6D!O-GhF_*M{rO>hy5{xmkRa{)e_@h(WIKh+f)wp7`UB}#!|E1j(czbr=UYDjH zelPoymRKU!xrSlsP^XP(o-j29(gm3F98r>hUTN;`+MhoG^MXmC}pJ66`P zr#c>l4=ALeNRP zYPgXV>`6Q(?`tsAc^*-GVFv056^ypIrM$Y;u&=S1P8k`@x)Hm-e$w&fc{*EtJ;g7R z?zGO$+&hX=aXe{6xh~wpkNIxKbY=H*_Z0tB6m9I!;Y_wo4JW5friy;&&k6pp!miJr zaL_`#X!6R~#jaBwOdHR*|Hvi^32fBbtmCq=vB9Fo^eC`_U~22dhYwW3=K~?9qZRc& z9g8A)Z_*U$@^jAKl^9}xWq$WZ2;{Rt$_NB#!%V-ZFwouC2f?h1QAPut!>Kd2j;vt8J0fxBb7uBN&5|^cRAbIII^>NL)zF&St8HR ziT`bwuo9=mv8TsgUebHdkbh@egcb5#l>?haUEBYZs_5x5Ghe%7EeQ~rRuf%lY_eIe zugqDi1k!l;*f>1S(6Bj8PEWA3mR5CQeHS=Lbj9nU_tuLdM~-)B90h}uo;dxTNzZ8f zbnKZLL4Gcg`)2>N*+ZaaY%WyO0htWTEsStP zL`0%uVp=yr6=--gvU5E=vrFkQ+K`b=30vvQ6i{Zlowhu&f+S`A&sflzlQ}ZoE=-)-#9YMz-upPAM z`YkPWDxkhbUbU-15POiPLe0V9ORti_VJN%eG4^xdBh;aQSoo@~VbPx2(3xBjB1Kmz&G-d7l=5B}j!k?jK=rngOA53e-_L|JTYhP>_=Us{% zOVK^~==b|9{Kg2>2;1A@*mpRIgLDKy6cCJj7ZZ+MNf{a7WS%0ZA9Zt%f2F0IVAmHl zh>vPbn9CoTw?G8o-Vp)$Fz5>*lR+fgccW~R;EpHA=#?zZ5e!gjPNs{be~aZ6#*v8< zq*g+mgKHI+sFaldjrp1d?NDSksxFes%u3@3sWIaefrUQby@CbS((2|W-uJLR!g_Agig^ygs-VVeL+Y6 zT-;z8WF!hAlfy$pTO;X*es^_Aq*T^d1_uWl>@7r@PMw-H?p0SC2S2`w z1vW+N=FY1xXlx9ulhe0JBuS21jJ#O!zg=-=<$v13{1;?zAk3w#^y;)uZL^7ucCGIr z?~98PY|uGo1npc~^0?1hCkeY;2n{Y?+}}ru=-7n=`s~V=*1Y+?GqwTOeS^ML*=Pg! zgb^WL0GE|a$hx|c$;$|S3d0cKstHIflDG#}7v2+k-75O}3? ziLk_6ySsZm{h^%w(ah}e;F(jM_NHCUEFs9(U^xU)KXjvjCM?bS=*l=Z}VXJ+4W8&dvP_vk*=x)bYh4Pq%_c~Qt z7xJ<(z6O5L392eWI=Kx_DZmbKWQWZ^1gEx?tq$0H-#f~=6m z!t&H12AD@c6%-m4*WzDo3J-(z=TT>IH|%bJU4Ae!0$r7isOXml7A*jIHpU8`3|<1X z3}6~I8JV1{>;pLdbwMfmO2eLY?Q=^1#m(D*l_o?%s;jHuXV8+Cl?}|y9Gq;dtpPTj z2>wsFJ)pJ&goS{71C`|GhlX&Nz}5q(Fjo6|kn~H6XK0nvkEMG@_dC}Ok6o2}@~^1J zH(kxoG91nif%k;gM0-&;z*ZocwPaUg(Kmu4GV!#+E z(z9b#bxv4hYi|#*8|Tt+n=bey?#?M>lYVTNy7mhW2KJ>B6lLGPe}@mM{@=jp=vS~) zY}rwwAsO{1%)^hMwDgd7ARtu|6h`u^b507ub}!c>Uy0&V_v-xnQMhIn`6r3x2)cNM z{#N<;>qlU_nl(2Mi%Jkp)E?l|e?sJ8fC8%DxD2wF5WNAAtMDigrHm%Q-Em#+`gHIB zBrVXz_YP7ub7Of8=t0Pz+}Dvb&Hr1Hm5tl_R0|752i; zCs5#coPd@1uAvL=<9s97OlvQ;-65WugV<+~spI(kdMuiHUIlWd9TX zOKfeq#ss6G)Aw*a0)#Q);O~qRa3r3Xa8ZA3XC#^(geQ{`^MCj6vT79cdB-#n~t&SYI6z8K+BbKU~QhDrWW@ zl%HW#3FWE3<=yl`UOrejCtZ;ano;0(2f>2Oo6#e!G8q{egakhOb$CL;W(cmMz~crS zU+MIPMEe;%c#=FW+i)Af!$9bKsSx^L2yLRL|I~P8?Hjniub@@!EH(v)2AFUG5nKJb zTB4B$Y+n$7KSLuUVh~6=G9u_o5~O{qmE)mQn^i zrE5}rFppe-S+0Q)t_tSm2@Z6~#m_AMN756IKc}WnFZ?*54E`8=i`v@SZ$E#&rctP` zrVV_G_Q5EGn%|j;Mg=`no-8eOVamfU2yO!qnvwJIeU*v`prebm8gVLEO`RASX*%AD z2me!#M=u-VnD*ruvye$(6EYa~uiU;)Ao4c-H=x7!$-M*1(qtd@$J=64TbYD`>}p=MH|i;Ys}xFw#ypPw$ItUz`uoSzWu)bhT_Vy&MX<~W_R zKX3DmvHDF|mEXRV05ci1O#u1;$7M$_85Frd%s9fiw`B)hM>v(`_2rpJ-rL*}s52U` zbLNpw4_{mR7!Vi;h~EP6^$379BQ}9l&?yog0d!Q0gZ2ofZivAFh!CLIy8tqQl9>`Y z2~66%0}@ZbIp_t62gSwV@YVox4@^haYlaW%G|!#|BhV^*0s=4(o`NVCs$jUGY6WhP zAy`;tWxGGW!y~}8ykhOIjOdwvm0Nd?p2uUFKehogJlF@I%n=TJsc;C`shHfdm;5)a zv^K4(m^<>Zl#gkeyV%+a{<_?efXS_69RX3LR>FsUd*OlI@mvq;a?Ch`)NwEetVOtX zY1%L_Y)z-D=X3BqfUK&q`&`H1WG6@?O9$=|&k`dUq~IM;$W&I|fqn<%U|K4wq7R+Y zD?RUi10GDzEXekQxDsqra9jeDLrNj+jtkv22buQAl^>rigKYo7>CFGHSt~(xh$aWa z=BHpCQ<3mxx9M~3S%e-m$Gw~KL&0n^ztR65d#6vM>&2bh#o^z0R9jIZoN4S9j?Ih| ze8TVH9e~e4JaY)#(ejy)`yl}qF!+gIF3}3lcj;UgH$3Ws!f#eWIC$GXa=zyzMkJ$c z*Y4W^M)R%A2~g)XxPG{09sL5Lj%|q6|bXQ zB5VK<{*TS4prnM`5x5z|1TB{Rj63f6;o0(+*2@N&_c%B3X)zfC0&tUMayJ0~?f^-?+q{HA0pppVh z%KsU$HjV~|hi#s73R{$MnLeh=armJogg}-f zTYGR~BOn}bY*Ik$U34@AvLn@ed@N8I^W_WG;1I`Mmn6hPfBu(QUiw)a-58hObf@B-P%R3T1Uj1>6(RaTomia_YN{M&hX`z3z8Yo7Sz76PvvyObr3m zFFp?uhR~NUz1&N{1*4Fz4DA>Q!3N|#fT#mJ_5csTWQ1V&KYs=n3HGDWgNrKVR$Dte zFr|SUcWLQCeX=JU=kU1+AjCD`X1xB(57z;-A`v{(e9FRN-gp2ph%jYA2^?JbV3ITY zC~I(TkH-kmzCe&l3U2#&#>U31#6eWlN0-yC zf;b{mtZf~HypH>KVCZaJ+*4FhdG+em*qSPkh#+YPIqkd+A1KtpW3!o*0_DP z;~84>@4shKV*O9$3sg%S*FR{_XbGU!-OukNb7P z)m-+f-em{p+cksiCWofdz~0B^W<`7-5THVQ0W{Bgrp^f^vna;&ZX7{dD-wq(B;G@P zYmXs3aW?GXQR80_V>Rgm%R(8f;_PHChxoIl@{dSn*tOKZqZ(_jYH1{ zjBgO7Dq?51`^6+6RZ&?Pk$nv~5Gco$U%%#@n-CCF%F50r=itBt6*{QRelU3qtv`@e z1nD1u4j}#jB^J?|hL53v%QuuA; z+YZ##vQK5vu)Xn})|7euf%o&HC$Njav=|Sv8DIg_cp0NAW6{pz^+{~rhkJ&WN)8;n zNami)1pY}$?UZ|KtFF(iCrL0lUFI!D7WX89kFNxjx_maB*mvr$`wYS0o3l5!!J$pE z`<#QF9p1U0d3ipmsS3RI+e~f^1+%NRF?77JJkZGffuG~Okjz!OG7z;-mgj*p@ zx9HH`#`By+?I$`vx|0}mdY)I}c+`UHLM~6r{yOgwE&P_TmUJPUVMJ4S^183-XVYMg zs%(!R)?d4AhDl1g2MV%ZZs*aQ29@+YFPGvJe_0}?pc#oCCEW&B1gLr5ZW@(LZUHC_ za8SU1#gliUCEGhXAmIKr6f+_W=0JnMybtgmoZbzyA&@E$pCN4Mh!10?&Kbb{k;3yS zGVJ=pL7AVKnbs+(-qx!0E>=wv-v)Yre#?)m>h6|>p0cDyCN=v%ZV+n)D?Snt3k3gU z|N6BA`ZC!hfvVY!3wYC5(X}|>t$|$}QL=7lHwb(F&^i_D`8t;i1Ta%Vmbw>orGP;# z?+qe^5nx)n`%rp^a4W!&0lrY-(g~sYa^ipgtR;gbK37L7A-@Tj} zz88a>nO`vahC}$Yf&ldJgb%(pqV=qLS8j7=7teO>xF0Dbqxa)~|IR;NeT5d3$4${} z^GoY{%T!!lW-(UO4@T*cFiEz+7aWD0SGS^~2qwxl1gEy=P_#yjF@V3;|2ItZYMb$e zh6XB>uYhm_%d0#Y_6E>;X2_9fzR5a0y)*i&%BL;xt<~}7CkVF=?iqEDyBpZny|?Il zA&|isxPm#FS7WQ_ihDHFx^S5lkivJPEsr#hYP6ru!ax_m7w{S_v z;>L=h50&3LHV$O({M9_~F&68LVuSHjg8awN*PFa;J*vxdYr6-qE2r~c%zs?dxIaPW z2fZ#ZJwPH4jJ4qGh!b??hEEYj{JRmI_-LUJGTVs24WSak!3>-~8&VX!7mURdFWUVs4Y(FlaKk=Z!3E2+(SRcyft} zJurnhb(>X9Z@*ZG^1Nd?-J}D=B~+uZ5^lj0=Oslvaj=bcEjRC+EW(TcZf@hU)S-1h ze}5>D@rH;MDyf(0${<}Wn%sherv74jC*W2-7KI;rSVE_|Y+R)O9Xgx4=E+j5ZH0R) zj+Z;Ln27ngN%tunHx08-VuN=y_&={cX>cR3EDC$uokUIBzJDY!W-svfP;KMd-Bj`~LZWlzw8wV}(zjcQ~{`sfdcPz|H)<6-%3s zb<+y@gsJb|`Dte{jp^Hg>lb>b+}-Fxikrvh+1u%ar)5ms-Qsd8c=Pk~g3&MS{-cMG znmS=7lyh*{txJLDcnLu%KeMv@TrZCGz@ifn0OxHSwC0A#$IHN|1-@z!Tav+Ccewtl zpbiRT9Zha{rv{!Amd2Q)Q-@yRR%ZzWN4+bGou$exi2%wJ9tUEGRf=r18YnDeg@q1| z3<$Htdot^M>!!^FoC^>CL;YNJ(3#(6rlQ(UhGCH_(?3XkJ3f;|S-8_ik=oS2~|6`lITBM{EW z-Mye97nAiayp}Mmu9|*kx9tLD=lj|rM8ZPxM;knPtY5gVHgZ&XhNh+@O-%am=ONw? zQp7B7u0r%UzhbOk!lf-0VHL|s?^|810p0C{4<8!Tq2^qz6MrI}_Ru6u+P(@&;kI5O z#c96Ai zMTzKRG~PuD4EP&j-X15ASv{fo*;=Tly{nZdTWGN*>4&*E^RNl|c#(j3*TH$z zux^yOQ>Ex8h~d4yzEbW8tn{$YamO=<%1y1?6dRA^Xd_ql#dDdw38^y}7#P_6TNa4e z*(fQ2m|JqcIdKQSr?<5DzN7o3W71#-=>wcl!P|c|Qp)_hD)4cQKQkq3v*M&Qya0a@ z^x%P>0KP4sH^*fX@BDg^^Ym! zq(LyFSw%ePiD6@eDrvYZV1$BTB%7yAv#STMAS4=?9C-0wIcb^Qc^_=$@lw@((X(c3 z?^2<`U*l-v3}5VaTgQ{68*Y0T3Te26&VLSybcc(eSb$a@M5r>Zs20|%&z8z3x9>UL zx(l{~iPH6aZ4+;`yu{7%yu{ie4S?6f^Cp8WcH$uD8m@!LDf@9( zoI83SF1vIv2Zv=Ag6Uzh+UuryPbop8uVW|uz^8Af&D$j7$l3H&e2{CViFf(c?8PHo zGNE0jW}U+bi5Qe=%H{pj@%_Cs^5nOb)mc>j$$~|xW64n`!H3sXZp?upZ!BkBPhi`2 zgc$~gYS2Onv7#H5OhC^J0KUnO_qjks&=v5IvC?6W0;bKh0ZBMX3u^c8gV*jA-|EE^ zXGO)7(W)@x2DW>c{h`x$RtdD*I@kLIh*clN@lf^$ zM|r}3c&qi?>GoXD&PiZgrQ4JPhvi#O`iS}+s#!6C7vXSYbh0~gGv3x2C`VqEp`E+3QPH~+U(rV{TR^sZ;C9k`?@dql+Ma_; zWZ0~bEuO-qQ^${pB7rjchZ_}V`7g0kCml$B1DZpN+M?)kHlV)Y;j_Z!Is07x|LG_Ox zx1dWGz9UFeLrf|#?I6tUpbYc?CkljPu@c6@ciL}tXKTyw_(`7r?7cXSqffs*x^AmJ ze0ovi#hcs}GB)DXH|>@Bn>XP;hhg{Jyzi6t5A0e+W6%O@fV|xR))zo`Ag)5QDkuR-+L#f#A~xg2`|^+23t{)rRkId z?cQF7qD_$w5m6lchbJJY{?GrT`srnprmW+J71wR}_V*3hKL3?vU}UlSH*9Haoa00q z13h5hxkyCcNQ+d8Zwjimgy=l~-5^xU`O(?wB|hSl%XOAM-QuUs`b_k$Ac3dKuIW4$;%5O z(01^Ixp^PlGW|0%y>K0U{Ajw+6Jj*D(u?w%4)=75oeozLH{jRYiPLVi#x8G;VzwZa z!o;`SrjCH9TvV;Q+Xn)Vp=I-izq1p)NF&knRKvG3HY_!@~o6Xw6xiw6hI*~8RFu7* z`PW#l;~N@M?@tpPTkdYAYZEwqQwUg?kchd9fm(C5yBzZBjk`SIclb5x?QbX$zX#-M zoXvfB*fx?E`MN@RNjW4YIwZ(BklC&^mxK*F;ZJzanl!B%H*Vz6ns@TO50A)JIDIsG zEr+3_;T+zv-9>V!EauxH?Rv%09sl<0h(>ob^8s9h6ac*>-(0SGo||9!fBUus_rVJP z>=`C;5RD?gdb9!9I2QL{7C9+#IonUEE#<+-X~lT$M@bu!X_B2XD8bfK^dqY|G<6j- z$K{2J3NmCYG#O*KML=Cm;3HyPiFX#OApPDvQsIaI1**evNoz5m6eOan%+|GJ<->mu;` z?Lj-0SLIXj#kY}EQg3yT@IF&0ADV`u3e_lTXh4)LKN7uO!*5;IVz(=r_K+uM-|bun z)(@wgHneKbR%9xt1!KHod$te|5Y+hW_+2-A@s+@SEOff8fj>snDw)KZnP;pQS< z>f5?-*Jsn8p5v0pE%ZB82M*K??k7XHpERrv8P}iPxHg(6Y2cNrJ-1=LeYO9!X{6_l z!$zZHk|X&kyY2ixz5Y*tZuCcV+i90J zf$=0o*_T}EU%v9|;)kxQ?n;wIk7O=vlsimfBauU=oHg z?E&U33=9mgEd|~(!xqKnx$i|ybGDFF88*7}@EcxebH)RQz8|wYV5ON(cs_K2;nNu@ zDbkcED$((vHVye{*M4Y#aC>`uk=^IpK0fAmKNnM_C@fgQ()#QE=2fh^O66oIhOzG4E`l%l#EJ?FF znc3O*8fFzB?GmBj;pT=OUV!f)WDypFIDY$QVE5NMKU{yywC=unJwf{1?t4Tg1M9nl zUz%*m<}O<}87uM&QDd{KX9V(!degrXQQDZ|9UuLs5We9oE5lNJeR!*=D6mk?O~T6R z-un7|Sd6|Cb7`ADaCF3-YhNxdbZ_ukSTKcvr4jPG7SrIB2ADY=?n0TEXa1rz-tc;} z%~?4EfC;k(5I@!TNuBCWU9PnyWF)>x-^Jkwweg!>{{MbNsI`L=Fo_yClllo7OS%1hDfFaJumYCY%%S-`2fzm?!Ij7KuKXLU zLWrPOViFRt72UqsXSu<(aWn#|&aNitQeDN*e-Nm<4)dFUEuUVHdG^ByC1#pq?-&=R0o3KSR{T>`HmaWM?zX;KWAMZWz?g3FJB=i_S zI4t*Lb{*HHN9~y*gKqS4_{vMBiWov+FSTcwa&rFAvtT6|SJu@91q2j_ZWA#)d-i^3 zM(cO6tls?Ot7oHi9|x>pV(=O*RKfRwhTP>D=O<*xXc14*c!C2^IcBd8Brcamyr&|Q zJI|}syOYCTe*ZM*V@&dQE1|N#F>Da^+Aizcqd>|JS0)=a`&c(+zN;0at4&-(mA3Pu z?X*Ykc%$pPg%=FR0Qc~`9&G+|z5BD6*gPE((bI022Q*^!y8nZUn4eikMMl}j%K|gyi7cjL%Ry=#p zX1HT@VlZHU$hx8_AZ$->b#R(UP5lGDaE#&exrjrk*kMGzDGBOoO z><&!wh>+nEOAlzf9+bD)!KU;Tak)hv@x!`$Cmv%(W~3r+gNbS1a3x0UTKQsww$Ld9-#g*C*|JS=q%8kq2PFl8 zix5{02ZjdWofk!D8mqRvAhJN*%r**Um6<8rn7dT%$KO%98KenFte$wDSAALdm6bWz9i+JT zak4f@B&kdMYAWqR+Wt{gO3_Ad!-Y|LeqKWyL;51AKV@W8zYJD<bb`9s14poUiWR8zwD%;xD|^ zOj1mkgt$VF9mi0~9U>VV8bGVvVV|hge3t(7!B=Re0KyNqG+rU7&lJpoL3aJlLKX}T zsqK-_RBh+hPYtVPLnEW4O14PV#wYn+s*TEZpDDqr3^fmk@L307_Cr4g%tor~$lji6 z_%4H0n9esH?HQXlukiT^BHF`iaf5+&pRx4(=i5~?k+d`_B*1?-P58&Fx72MIptj6} z_JypDogHxq-o8m+?5hB=GJS`P3_~7=)^qVQFVXN=2pK;BB^wdZykL$GmOm%A8Z#$p z@io=kc+2hbv2mk~Kbmi7-^0!Rf)drS>D=g@QewYu>$;=)#;o_DYc`>#f)5KwFM-nq z8VQ(b9}^RSwh%0W^|xz=4#+%!G7fLKz-Fgkp>zbZD+iJWoHu?fyZ9uC$RwB{98Z8{ z%*ZDbd(%SA5tSB9K^GRSVijNqDL z9HzKjhNltZ@sKP;QX)U@%a_N!^9`Bu*9NMp9o1H2Ks^}+^>iLRLa-`pzdZE9QIYu}rE6Z}WxvGrpU|2S!tkV>f>KR4opaAql0zyJbAU^u8MKH-oB#Z zVBX7FqHHfU^`6`A+1!V`0S>*w<3>ZrnCAa)?~!C)f)Yp-42BHlFCAdFe7Yi;vE>IZBZxJeu!e>u#BPiNpN!R@rHHMR-k9G0{*_NsY&hHZ zlq%Np91b3yaQGpAE+!>AyOQ2VMn2+Z4m&O`Kx7$!?hxwd=>M`EK?e`z$l0|I>~#QD zX;qmokL5VlPrsAFVeX3_``P!=>L@8W?zT(hA>yTf|9W>c#y4EQQ6~%_5~Rp!Cw@cz z{th91TzZ>d978nPeyv z(F8<-rZ7O-3~6s4Iu=71Aoyy4+dQlxm8@~FnSqvM%f+~E&K=H{m(M2lg?ySX17GOj z`1oqz=QEx`wVt(vT!<$)Y+?Dji5Q{*G#<(qO+f6*Qy9HrSzwqbrJ_RcAktipwATUR zNXUY#a*36a8qdP0_(9_Ud|NQ&WYuma+VuFAq;v-&XoI0Jg}DQ#>@J`p9jx}r=KVh| zz}wq3Pp*8#;a5Eb<3VuAIn^oyW(G8@1Oa6vVIfQUG||(_bNx7{T;64mo|F{Y5Lp11 zZf}QW>_PVnJbXe!3GSkz|?Ykvjwq=jG;xHa32y zv7ZIQe`*ZLxOBB>h1eM40|^d{scf7i3%U3ATf99AD%e{av}96` zoMA&YXgnK|rR&bQrC@H1cPrq>&1! z3n$LRs9GwPDn#3+q$GwF{hF*kvvb$1+STUMWr}rRjdQdT&fx^iX1KLLh79lOfaq=f zN*mq%PKG7U=~Sm^zW&Pj6MC1&`R71@W_4mujO*xNlYs84Kj+0^?5yTzRsP8Zd=#U= z@6T|b(T}%7bO@$A($t_OFF+T}a0z;VJ#cV&4N`holMwczAk$q|=Ag@e4ylLS% z9C$!?!wC#`YB=IFJoK{x=TaaRGKN9c2837e;ZwUQV*qju_bmT;>UNdGb`Vy<2Q%Hu zh?~Pi$%F>l0o@N~+r+3vSxd?)VR$7V+vPy_bgbfy&zaA~1@rS~&Lc8RhKn4Jn=2eim z0mel{#KfSz_{4372~l=`e#M)ehY=8iC!W1MOu;OiSV1M#5D8PtFOq);zKEj*#Q6IU zUV%O4odp@kw>FVjp zp0I|qK|6#`!|w#z3vJI1?n)yAGW7KUL(9o&9S{zZl`6j|^Ic!}zpZ3vXL$>6spZO| z<++3wRg?^JGXM4MIIFkoS%Jeqcp<^24_P_3B0QfvjYiCN*b1O;fY{S*Z4LMON!NyY zX&%n|DWKj9kGJqSl>Kh*OijGIvG_K9k*dyUP`CAb0vxx*KI_a{+3jRBuTcX62vEIY zjWzR(U4M@TFAEH}^XG=w7tc0Nt}V1NgJ8Mf<(qh^OYTMb)FFu*RpP!LTIBb-4XULq!BEwsKX=LR z_&d6u<AjLg-x3kwTSjJlkCL2ysEERF~08tB@nzZx!BH<9g?G;r_s}w%cRBX!dj~@ z-IspEuPIUDg*~16UiH*P4aN6n%{=XTLXp}EQ7c~m`}*M5Z-M0%_Xd%Z&OIcs(;pqX zE2qrE#iDFJht4qFuqrB|j~buzL73|u`o8v0z7{sC4-cK5@KmpXNhKQq<)hP@$}l#8 zjUl}6W!T$3cDRlpE-FcW%gxve!QER0>bYh~i$zkhxbGeCL&G{h_V!gT)s=J=v-^+alU?wmu8KAktMA#`K|G)%}qs9s#hGAt#DIKbV7pGzMIlq0)ez8x{ya zLZDf_=LVh{KpX&d_c~BEDouKDfiN<>FRJF@#=q#BYr`TB^uyP$UpL?UbC!~64V){< zm{8zVr$TKNVJ?f|bc9NRzDB!i54EVi#L!G*7>!YHzn`+_mlEX=hwl}U_~nV>$5*^d zttmKiv9!lRe99u#@0#rO-f6R)ylthGCn`?;HX9H2l@1+5RFbBFnJg5=LgY*$Bx(UH z3Ny2_-)E}wnK;_~CT6Qndw!(uL+t2+BxV+D6l4NF+aVqP{lZV4XXROpEoeY9N`r+I z*xsWVWn`Fo-izPiaX;89EwNYOsAn->KRhu-h(g1g5aqBmB+%BajJxr*CW+F|O_twjFI_<5oRzE$yB zXDA_*rvC@iIsf`@geaJGvC3L6n1h0XfNu$3vyzp^q#GMfb?`W_;a{q$b?(j}>o}FF z7Xd33iJHlt#%|+ZpqNb1X?=1do_Ob_nM3*vd(a4jf4xdVtnEtXrTzN&IY02cflmiy znkuX7+rL{(PS&4>w{UEa@9>&NDz|t=_~$4FmfykV9r=>Q`0n9kdb$;6USXlv@X3N0 zm}&aMNjCuc&aPjB$W~WYm>BEhK-Np6$D;-l4B$p#V23^8x2;d@?ft^F7wvcg+u(I; zNH7owwZ{8NGf4X=uFMBLZJ7qwqq_#IoFnz0GDzSD+uM0?;VEARPRyKO+e;4ASk38U z#nL0o`+t!u*M4#uu!1gqn%S#QYsb9)D*f=t*Awi?5>VLYOH>QqZf1snyZ_RdvUnFn zhJF650OcuyUc~TbT>+4AV1dQP#qoUcdHy{~af@DfH5K<*47J5J@7Ibn{Tgltdj+Sp z6h6uAfa{t^@^?YaYYL5H5Oq9&EnaPW36oJF7V|hMgX&CXtiM0p>+0aA*w~ft2(aMg z6%FgSzRG$ss33WMxGPn7}sxRF;`t&;E$vQvDP# z-tb)sbvV{OeY(lS*WWJiD}5=iIDEHk5ZnmpJvp9rJqnL8k*NP{DpOnT=EZdsyQ7(^ z6@DP3WIURz80mAR?<4IC$R;tl$PUF|4 zjvmsS3>YTTCF<`-8$x*=%Z0n(Q@EK->hW3cy(JeTYC|hS37aSW>aMr&GNZ`0>2&PP|20w4MncIeBbv5sEXC%_PgmsL+ zS(Mh%?9H@DGwK7xFXNIvFb&#fS}((s0;{c_Xp{&@t3U@m%$g_RIr>OUcdK$n=u__J zC{~67wCn36P>iBK&o_C})6?B;!cB({w!)g4k7Z2m$I?QV7w!1!XR&@uT5TVA+|pB|qeY*@VbrgD%F^)z#Nkge-l*9;PmMBEJwr&+^BEKhPEKm>p6$DdY=<$}~MLQ4K z4S4ugiV(LxtAhqAY!Xo=FZt<1(!Qi?n4abU; zpl3BG7f<(e+?U?<+`*%oA5{fgRp4b5EtKpWxi8lwGL@9bmPASwO|81>yw)0s@lHK! zNu6&MizgjJuM61=kByDBv~6}8o?mtZGjTBC+~#@DQ&&fL(6pGDNb58m7(3ER{00^z-w_HeUb}bS2 z6|FJSQG#92?tTS=HP`}Ah#^1D=8BLGPdHlVxDiD~eZy_mKTd$P)CeZnRlvz@Db00z zybIbT)V7;$Eh-KSI1qM3N`P0?j<23{bu~rg57~=juN9ue?2+U@k2xx=GZPGMc3Ef| z)ZZL9KyM9nO;C1&+Y~5ZMmLTMqbVNs+3zbWDMS`l+NI{M-G z<$WPIP4tty*(~kD(D{1_XX3rW8zYRv#Z2xe)19T_=pctpEzvR9=kspUE-^pdJqLre z`z3D2gzupcMyOK6{=Ib=J`D~Q0yWCu_QZ~CgM~l-yh*fZ&|TA^Z8<`oQTP#4RM9rBO!9PTA;B_A+d8f z++)%B2}u^X?QO$s=Cy{Y1uVxT(DzefFU5|p|DYYQr6d;$Vc4;!hQ zqY9J-9&OmXEvg_&wSU%IMoK`?4yL1`Up8oI=|&Fzd?UZuzoCDX`xJGWD5!hDz+BpB zTcB0qZQ%VSavNjidB&Fow7;9YNeR2R95t;&?;JVZP+--Y?TG;4Rn_W42R$yc-_fNv zn2+g5B)(9H?S#Nq%_{S6uL!^1vaAup#QeSM`m%U&-(;?q4f#hQ6Jm7xJJwXr*h=Z* zdVyQ^Q&nUxBlaw8JJU~y9^ivjN}s%?4e^cd!8o=j9y9Al6+`>^nh6C7X1vJ)&QpI| z1t#@~>Ip1XE;zLbU6|uLV^PPnDygGOQ4CohIvl)SCAF zzi{4u&yYxl9Wc_n*m8h22cTBALu+VDE9FabdPSc1Tlf!k%}5>#_`xZ9uI-quL$r3Ou zIL$+I$YD_oqA!3@2h9*Dwcv&-ede_)ZXyNyzJ|Suo_hGVyOHH7hZN&N$WPVfkXZjf zQ#thOuiI_jY0zt1XkxLP8!zDbBC=ZV^MBkQ7lhjl zXRzjjZpBZ76cW)hElnS$r$PA>8@yogf7jm>Bt@ccqRh%#x>F`6OUWFy@}v{o9|@9z z!*cW4N;pISbKA{USNtw@#KGrz{f&3+?F+(C(-h7%B-}>xFaBB;EA%dnF=e-=pxh>N zXePo~LK3@jBpDox3=m7y2R>|GljmL?H0J>}(q-THaLMxDkmS7QX1~ziD1IG^3TIoj z!cyBUX5u@{#K{jHU|jAPNT?u$gCdi9#H9pqNWju2j5QLGlN`F|zNLx-YIIRb<>$|# zSYMk`OH4>x9~8I&-n>`Ws-6~I9WK*!SY)Kd;Ab5VyZznja+@hn6VbBzWQL6wN1Lb3TK^+0Hzx{w;HOi61#N*uI_CEB z)MUM5Chg|xSpE>@X~QKL1^^_o3bgPR%CQ+MT+?{+-PVr2IA{T>9 zu?%o#dnHO4M;i)F>yWjpc4(@>nyFxC=i1gYIjQ~{ic!!73U2PO>NSQKR{B`e#MTWiHT@g%jw1~RL2Fe^EFNqjx?&{nnuD@DVbCKv%>(q zT2#H~%otNuW&@KM>d{xzbFp+Rz>Yx{QZI_`%IA)BjD`s%laLikRpD#{p!dT^;>qut zuGSG<)Fj0)f5P&uleP8rP_4&CPk;%MMLSIAFNT^k+%irOr5U#Rg?CA`===b$ItGUp6*yI6oDo5c_=SY^Lk+{g3y-WmE5 z!Yg|`Qg0o<6+wKTo>j`@lekz_&6@3B`g3t{FI5UUqstvlk^~&Mpo6`rF_#RN4cmjG zh$ud~SAZ6yqo%(gRpt|uuT&Y=Us-WuSq01xZ?UM$Yp<2R&K>*t3JF#!L;ww)|D*Z` zTJj(7PbKSp-U}}gP!~jID5FpGvfAz1oicsw>a*i#fmWnKnWrVJGkvjOCsLn;pqP~{ zj_;fZ3(Zc~?6lCFn9NouiN@Bzc~-qdyJxnBOxDaQiCXtrwKfH0_L9dqZ>%XjVq^0j z9t!Vo#w)Vk#yFOPhOsFVrd$OJ>6TJc-kRxT+l?6dufbK(vP1|2W$)!S3wt`Z(sgy* zO8{@mm*tIL%JJ}h6RT-J@8>V2*dIrGQAxxM{;)cNLdbOLY%NZbmwyu7j^}>Fe?0Ki zCdNOS=Dz~Pcx2SoKEB?aZl^gh>k31hm~P=yQnshQ5&IfJ`bgNA9z&yQejZMh`N792 z2=pWLJ<@oi+{eJd-Y`Z1C>v|K3V%GJ-`JO3*IuoxJZlf3%JSG|V%8{LQu7hBAmK3Y zUX709GwS$qECCmhx-@aqZn#Q27SlyOL(wot*4abxu_1p5yYT#IZV^|~T~CoTFlxE} zE3P0TqaIy*ew28umHitaC$(4cc#RyOO^75DKu@sG%ZVU67DQS0s50()MkBh|>>hrp zL_eNhpHgNt!j5C~zxf~=`h@TIz!%;}RZh(J0i{uuL7Tz4_kq)ud3j{yK6dsS@8t^` z|N0l8W(($}9qiqEc`lO8#k#p_XU9gPcnnA!6XUd3mwp{|`hasvla=MMxbfy_unuB_ zM7}|V2JBM%;qRZX@8Crn!jbP$BZ(Y-DP3Qm)!JGheWskLDbe})QOKLS@{-5J8x?X5 zG+V$6>5z*hCjK!U68fXjVZ5xQRd-XTp*iXC!>99e7y)n!HF|+0PaJG2hx+uTm?5>X zvA;#;M>F>qbVwKALNai1k_^D)m}P#>vFMRY6re%1c;Mnf4em?vd#l4fTJaj zx-3%z+x@M?+nbjnHW%Iwc!B(0(()p`CqA?IJ%psv+Gc@2W>cUpoH_Zd&3eU7uKB3&6|;#W}DeDIo#73WFzC29FA zpJh-cZ#h6POhG}1b%h;~WuMAOXOX`Pr=no5q)HhoNzDuM;xtCx(|gAXSTss$usL&D+IRU) zl%svCHnDUFC(s?pDJ{+GS%n@a$RC4K%-BD|o&+I8 zgpgA$G3Plq$AHoCwP3;;0y?^C?R3zt=Mg7``6ppHH0J$Ne=NL1e-uuA7-Wdq?Xvu+ z?lS#C-gq4Lc7iNFjgggj;}1Am_*{hEql2Gf6C!%oVdn++8KsoR1d%e5kWmR<&fwIM z3%vs<`52VbI&JymQc@zoe<{HXZ0$iO45+;d5CDMv{WVTmEHsM|>O@dtqXoPR3`8j` zEQBrxIT&4HSjjUj&(bjo%wCl&zINw-zICD6_)o3z#^>Bq{^6fDjW?zI`s_bNPaP;h z&^N$wL$i6@1VrQ**;cn#w6|V2q+G3Y_;xPWS6;o-cUbCUXq(xod?A64X0bCV;6cH8 z2fP4|dLJ3kv9fxhc)HR_AFR68EZ69D>=#R`VUVC0(aLReR>u&I1k+_tMqT+qla-~VqX3G!qCnfMRfq$S~90>06Q0jsx zm71O&O+FTSA_(VjDyktk=3Z__I%Y%RhnzV~++0wWmXy7@c^}#Y0Y9sOq=HL+_5oy7 zkrG3tYfcUR>_`2BsqD8OOJHQK4>+uV&0+-W-!@%x!*>7N><3?O$;CcMKP=ilsG?_6}%ZAhrn9{?*mhAo;xQxdhz{_C%KQkE1&$P;5b(eT61K`Ow`5#kLAj z5NKFTwSeCIQdgJN|M4yS&}SD61ripGg}|Etv}UVj@9Mk?tOKHq9sGrTu3khZE6B%Q z>^4zj1dvcUJmheg^oxoN4JF=}fN~HYa076x%wmLk33S3hn+F9l1hoT${Oh_CWSc|y z@(QR^;dlVU2_!oJp$@D!rW|Di16>@gwZFd~E*TiJ0q)<0RF6+>zH*T@89@KJh;MQb zNcDgqMMjnHmM?07OvkSKIAmab z_o$wjWipKH7UJuHO3-vDO%!lS@Y?wTvj-0Eb#sv*_MdNXRsu~OC}8!=WL1cOL*w?J zPaYUVvL`~mu`CP&VAa4?H3W;cByQ&irQCR(FC2<&m07IA;aN1XQ~ST)N7 zy)VT!Pd-INuyXS-L>QMrC06R7rKN?Cg4@jjY(N8P1q|t4cfmj!+)K=>4(5C2b)=Ax zFzMu_B-EUTMj(uF#1aJqRG^nDkkJC09D>LViOY~U4t@i0khOa^+gP{YMkXkj$#UnB z?woydCw(QMDP9Xb|1H{QL|=jpp1i3BtzykkZXD>t5Vc75Bo#!6wtBNbBhRCHFug4h z8bZ<91}Muij|RA*$G!;iwG+#i1!_R+Nd(>sRX`fFA?EABe5-@%=1dlPs=(_5^99h< zNdROC*Aud`>RM0O0O4@#Is){=mLEXKM`8Y08u;QkE@r!|_pt70af`^8SpE?e(ap~p1GaM>`q)HeEbK#7`NTn&U)r={UcNe_soBE}2?5he^QcGLg~dDVw8!%zsjD*MH$-{fn6t?-f~-oJ-FD^BjY z{$VLYzH?!*v8oiJvvpg7*VhKW-)0jIRJt6jSUezY_$&H2x+ibo2_~H zam(n_P3+R+)dc?&h&dxTYiqrc{U6wl^F)uQA09=zZK#sFhY8 z;+hy2@mXBWYseZB6E0g`CYx*g{as0o%KNzWVa%W49%12V%bS0TwmYAZ_%&2i3ZkMG z6=H9{`)nZMgM?J>tTd);b74ML3#^8*TFYewJh`jWm#S{q7sW-(c2V5Zm%NW^V(tB96AJkeYk7f1Qth zWr&?E4K+Gj*edQQJ(qF$J%72qe{ITW8>4Basu|cQ)_FTd3+#`PHAlPsav-}DiXZyWyK_vrOa zg++fiVME9Pm%`+`q36$rQd1%*nT~F*7obkE&{ib~a~?SUr3G zym;<*IYD&(6OPumx^{NTb{p$RNJx+MS?@s>Ni01B1E08f+i4BZ^vN~Q^84D17TwKX zbLA?FivnA}kHVm+(IYyF%2zr*{(I(aB&4$j5W85qxTyKG8RS_p$15;Bh_p5~ehPJf znuCKwsbXLN{>q`BnO39QULhJ<>0a+Yo4UCYWb{U8_70LiMnduf)!CGRjkUD`m|4Mm z8WaHtreycDGc%0W?xxn;=qG=D0Yay$-Lv2qsM~JX@Hjul zK|)%i8{>tiS65bMb#+NkjE{e5XmH;<+ubb&i>-V(tH&oM4s0AT-3w=mk+XDjQwNU( z%#d%NlafXtLan}3T!KDsfu)t6Q+R1fkC}jfcMO-dV+4S1}m9aTg@M23| zo{DB#q>C2M&NkYW;vyk^C1o8L1&6nMLr%uJIW`!U>;1%LRa)^0358$2$Q``Zee~!N z@Lk8=A)~A5>J}#pI9MAQNmNx;fhR=2DY&twrBTLz{Mfs5!kqfNsIpQO%tqllEIVq# z3xP(HEr^Nn`rOvTLm@l64#*gVZ$)ai)Cc_iZ%<6{6&4go!<2rdt6Lc!5BEdFtM&R+ z2MXVk%VXBNp_{-?RxvP0x1CFXTi|tGX}{Q7^82?vOctq!nWo$Y4Gn@_#$4ceQ`p+7 z57ok7y@5(rz2>(n(7Us3k`i;v@6_`=9k(7z)20v{&0b$O>oOiwpeOnGyex_i0#Jaw z91IIi7rk)cg%|J?j3iL9ftw70qr;YW^=X8`eiaQG8nI^JB?l$8D(y7KXB4RgMnhA znLsyJJ5?+2V`#|6%S#vN;lNgnwffBc>}5befK!960`1*OJ5RPwh*GjLHh z%Afp-kx2E`t9WZ08vzl}PRfCueW`K9r<{p5^YF&FlpB-Xp_v z+&)=-j#Xa_x3RSRu(&@3p;=Ky{x3(SQQ)y@)EGIbcTb-5nX1BT8l5+y(!37)bQ4k0 zjXU9Cr1EdJbCVFTRw_sPFmWJvYs(7i^sPvNpC63M1#$6$@&EgEv5t?6;jhK>Zz-S9 zElMI9nBGW8h->^+;=iwov_u_/dev/null; then + micromamba "$@" + else + conda "$@" + fi +} +mcba --version + +mcba clean -a -y +mcba create -n confrank python=3.11.5 +mcba activate confrank +mcba install pytorch=2.1.0 torchvision torchaudio pytorch-cuda=11.8 lightning=2.1.1 torchmetrics=1.2.0 -c pytorch -c nvidia -c conda-forge +pip install torch-cluster==1.6.3 torch_scatter==2.1.2 torch_sparse==0.6.18 -f https://data.pyg.org/whl/torch-2.1.0+cu118.html --no-cache +pip install torch_geometric==2.5.0 h5py==3.10.0 seaborn==0.13.0 rdkit==2023.09.5 mace-torch==0.3.4 mlflow==2.9.1 black[d] numba pytest --no-cache diff --git a/src/data/__init__.py b/src/data/__init__.py new file mode 100644 index 0000000..46b8b33 --- /dev/null +++ b/src/data/__init__.py @@ -0,0 +1 @@ +from .datasets import ConfRankDataset, PairDataset diff --git a/src/data/datasets.py b/src/data/datasets.py new file mode 100644 index 0000000..89454da --- /dev/null +++ b/src/data/datasets.py @@ -0,0 +1,318 @@ +import h5py +import json +import random +from copy import copy +from collections import defaultdict +from itertools import combinations +from pathlib import Path +from typing import Callable, Optional +from tqdm import tqdm +import numpy as np +import torch +from torch.utils.data import Dataset +from torch_geometric.data import Data, InMemoryDataset +from src.transform import BaseTransform + + +class ConfRankDataset(InMemoryDataset): + """Default dataset for calculations with GFNFF data.""" + + def __init__( + self, + path_to_hdf5: str | Path | None = None, + transform: Callable | None = None, + dtype=torch.float32, + ): + super().__init__("./", transform, pre_transform=None, pre_filter=None) + + self.path = ( + Path(path_to_hdf5) if isinstance(path_to_hdf5, str) else path_to_hdf5 + ) + + self.dtype = dtype + # empty dataset + self.data = Data() + self.slices = defaultdict(dict, {}) + + if path_to_hdf5: + self.data, self.slices = ConfRankDataset.from_hdf5(self.path) + + # setting everything to the same precision + for key, val in self.data.items(): + if isinstance(val, torch.Tensor): + if val.dtype == torch.float32 or val.dtype == torch.float64: + self.data[key] = val.to(self.dtype) + + def to_hdf5(self, fp: Path): + """Save the data and slices of the dataset to an HDF5 file.""" + with h5py.File(fp, "w") as f: + data_group = f.create_group("data") + slices_group = f.create_group("slices") + + for key, value in self._data.items(): + # save data (incl. type casting) + # NOTE: str need to be stored as byte + if isinstance(value, list): + if isinstance(value[0], str): + value = [v.encode("utf-8") for v in value] + value = np.array(value) + if isinstance(value, torch.Tensor): + value = value.numpy() + if value.dtype == np.int64: + value = value.astype(np.uint64) + data_group.create_dataset(key, data=value) + + # save slices + slice_value = self.slices[key].numpy() + if slice_value.dtype == np.int64: + slice_value = slice_value.astype(np.uint64) + slices_group.create_dataset(key, data=slice_value) + + @staticmethod + def from_hdf5(fp: Path) -> tuple[Data, defaultdict]: + """Load data and slices from HDF5 file.""" + data = {} + slices = {} + with h5py.File(fp, "r") as f: + for key in f["data"].keys(): + np_arrays = {"data": f["data"][key][:], "slices": f["slices"][key][:]} + # some casting + for prop, val in np_arrays.items(): + if val.dtype == np.uint64: + np_arrays[prop] = val.astype(np.int64) + # uids are of dtype string, so we got to handle it seperately + if key in ["uid", "conf_id", "confid", "ensbid"]: + data[key] = np_arrays["data"].tolist() + data[key] = [bs.decode("utf-8") for bs in data[key]] + slices[key] = torch.from_numpy(np_arrays["slices"]) + else: + data[key] = torch.from_numpy(np_arrays["data"]) + slices[key] = torch.from_numpy(np_arrays["slices"]) + return Data.from_dict(data), defaultdict(dict, slices) + + @staticmethod + def from_data_slices(data: Data, slices: defaultdict) -> "ConfRankDataset": + """ + Create a new GFNFF_Dataset instance using the provided data and slices. + + This static method allows for the creation of a GFNFF_Dataset instance from individual + data slices rather than loading it from a file as is done in the constructor. + + Args: + data (Data): The data to be used for the new GFNFF_Dataset instance. + slices (defaultdict): The slices to be used for the new GFNFF_Dataset instance. + + Returns: + ConfRankDataset: A new GFNFF_Dataset instance initialized with the provided data and slices. + """ + + gd = ConfRankDataset() + gd.data, gd.slices = data, slices + return gd + + def merge(self, other: "ConfRankDataset") -> "ConfRankDataset": + """Combine two datasets. As done in `https://github.com/pyg-team/pytorch_geometric/issues/88`.""" + # NOTE: alternatively use torch.utils.data.ConcatDataset + if not isinstance(other, ConfRankDataset): + raise TypeError("Can only merge instances of `ConfRankDataset`.") + + # merge `Data` objects then collate + print("Merging datasets...") + self_data = list(tqdm(self)) + other_data = list(tqdm(other)) + data_list = self_data + other_data + + print("Collating datasets...") + data, slices = ConfRankDataset.collate(data_list) + return ConfRankDataset.from_data_slices(data, slices) + + def equal(self, other): + """Compare two instances for equality.""" + if not isinstance(other, ConfRankDataset): + return False + if len(self) != len(other): + return False + + # compare data content + for data1, data2 in zip(self, other): + if data1.num_nodes != data2.num_nodes or data1.num_edges != data2.num_edges: + return False + + for key in data1.keys(): + # val1 = getattr(data1, key, None) + if key not in data2: + return False + if isinstance(data1[key], torch.Tensor): + if not torch.equal(data1[key], data2[key]): + return False + else: + if not data1[key] == data2[key]: + return False + + return True + + def get_ensembles(self) -> defaultdict[str : list[Data]]: + """Obtain ensemble-wise sorted data.""" + ensembles = defaultdict(list) + for sample in tqdm(self, desc="Get ensembles"): + ensembles[sample.ensbid].append(sample) + return ensembles + + +class PairDataset(Dataset): + """Collect pairs of samples from dataset. Per default only take pairs from same ensemble.""" + + def __init__( + self, + path_to_hdf5: list[str | Path], + sample_pairs_randomly: bool = False, + lowest_k: Optional[int] = None, + additional_k: Optional[int] = None, + transform: Callable | BaseTransform | None = None, + dtype=torch.float64, + ): + """ + :param path_to_hdf5: path of hdf5 file storing the data + :param sample_pairs_randomly: If False, all pairs (i,j) with i int: + return len(self.pairs) + + def __getitem__(self, idx): + if isinstance(idx, int): + index1, index2 = self.pairs[idx] + return self.dataset[index1], self.dataset[index2] + elif isinstance(idx, slice): + new_pairs = self.pairs[idx] + new_dataset = copy(self) + new_dataset.pairs = new_pairs + return new_dataset + elif isinstance(idx, (list, np.ndarray, torch.Tensor)): + new_pairs = [self.pairs[i] for i in idx] + new_dataset = copy(self) + new_dataset.pairs = new_pairs + return new_dataset + else: + raise IndexError("Invalid index type") + + def __str__(self): + return f"PairDataset({len(self)})" + + def split_by_ensemble(self, train_size, val_size, test_size, seed=42): + assert train_size + val_size + test_size == 1, "train+val+test sizes must be 1" + random.seed(seed) + n_train = int(train_size * len(self.ensembles)) + n_val = int(val_size * len(self.ensembles)) + ensemble_uids = list(self.ensembles.keys()) + random.shuffle(ensemble_uids) + ensemble_uids_train = ensemble_uids[:n_train] + ensemble_uids_val = ensemble_uids[n_train : n_train + n_val] + train_idx = [] + val_idx = [] + test_idx = [] + for idx, sample in enumerate(self): + ensbid = sample[0].ensbid + assert ( + sample[1].ensbid == ensbid + ), f"sample[0] has ensemble id {ensbid} and sample[1] has ensemble id {sample[1].ensbid}" + if ensbid in ensemble_uids_train: + train_idx.append(idx) + elif ensbid in ensemble_uids_val: + val_idx.append(idx) + else: + test_idx.append(idx) + return self[train_idx], self[val_idx], self[test_idx] + + def get_ensembles(self, dataset) -> dict[str, int] | dict[str, list[int]]: + """Obtain mapping of samples and ensembles.""" + print("Calculating ensembles ...") + ensembles = {} + for idx, d in enumerate(dataset): + euid = d.ensbid + ensembles.setdefault(euid, []) + ensembles[euid].append(idx) + return ensembles + + def save_ensembles(self, path: str | Path): + """Save ensemble info to file for further usage.""" + with open(path, "w") as json_file: + json.dump(self.ensembles, json_file) + + def load_ensembles(self, path: str | Path) -> dict[str, int]: + """Load ensemble info from file.""" + with open(path, "r") as json_file: + ensembles = json.load(json_file) + return ensembles + + def _setup_pairs(self) -> list[tuple[int]]: + """Initialize pairs to draw samples from.""" + if self.sample_pairs_randomly: + pairs = self.pair_generation_ensemble_random_sampled() + else: + pairs = self.pair_generation_ensemble() + return pairs + + def pair_generation_ensemble(self): + """Add pairs all pairs up to permutation""" + pairs = [] + # get tuples (i,j) with i, j stemming from same ensemble and i < j + for ensbid, idcs in self.ensembles.items(): + combs = combinations(idcs, 2) + pairs.extend(combs) # no permutations + return pairs + + def pair_generation_ensemble_random_sampled(self): + """Generate randomly sampled pairs up to permutation""" + pairs = [] + # get tuples (i,j) with i, j stemming from same ensemble + for ensbid, idcs in self.ensembles.items(): + # get energies + energies = np.array( + [self.dataset[i]["total_energy_ref"].item() for i in idcs] + ) + # sort by energy + sort_idx = np.argsort(energies).reshape(-1) + idxs_i = np.array(idcs)[sort_idx] + idxs_i = np.concatenate( + [ + idxs_i[: self.lowest_k], + np.random.choice( + idxs_i[self.lowest_k :], + replace=False, + size=(min(self.additional_k, len(idxs_i[self.lowest_k :])),), + ), + ] + ) + nn_combs = [] + for p1 in idxs_i: + for p2 in idxs_i: + if p2 < p1: + nn_combs.append((p1, p2)) + pairs.extend(nn_combs) + return pairs diff --git a/src/models/DimeNetPP/__init__.py b/src/models/DimeNetPP/__init__.py new file mode 100644 index 0000000..011e0ca --- /dev/null +++ b/src/models/DimeNetPP/__init__.py @@ -0,0 +1 @@ +from src.models.DimeNetPP.wrapped import DimeNetPP diff --git a/src/models/DimeNetPP/wrapped.py b/src/models/DimeNetPP/wrapped.py new file mode 100644 index 0000000..318bbaf --- /dev/null +++ b/src/models/DimeNetPP/wrapped.py @@ -0,0 +1,59 @@ +import torch +from torch import Tensor +import torch_geometric +from torch_geometric.data import Data +from src.models.base import BaseModel + + +class DimeNetPP(BaseModel): + + def __init__( + self, + hidden_channels: int, + num_blocks: int, + int_emb_size: int, + basis_emb_size: int, + out_emb_channels: int, + num_spherical: int, + num_radial: int, + cutoff: float, + **kwargs + ): + hyperparameters = dict( + hidden_channels=hidden_channels, + num_blocks=num_blocks, + int_emb_size=int_emb_size, + basis_emb_size=basis_emb_size, + out_emb_channels=out_emb_channels, + num_spherical=num_spherical, + num_radial=num_radial, + cutoff=cutoff, + ) + + super().__init__( + hyperparameters=hyperparameters, + ) + + self.dimenetpp = torch_geometric.nn.DimeNetPlusPlus( + hidden_channels=hidden_channels, + num_blocks=num_blocks, + int_emb_size=int_emb_size, + basis_emb_size=basis_emb_size, + out_emb_channels=out_emb_channels, + num_spherical=num_spherical, + num_radial=num_radial, + cutoff=cutoff, + out_channels=hidden_channels, + ) + + self.linear = torch.nn.Linear(hidden_channels, 1, bias=False) + self.cutoff = cutoff + + def model_forward(self, data: Data) -> Tensor: + latent = self.dimenetpp( + z=data["z"].long(), + pos=data["pos"], + batch=data["batch"], + ) + projection = self.linear(latent) + return projection diff --git a/src/models/MACE/__init__.py b/src/models/MACE/__init__.py new file mode 100644 index 0000000..fdc0dee --- /dev/null +++ b/src/models/MACE/__init__.py @@ -0,0 +1 @@ +from src.models.MACE.wrapped import MACE diff --git a/src/models/MACE/__pycache__/__init__.cpython-311.pyc b/src/models/MACE/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3f05c5464786c24a67186362d8e0cc6e1901e35 GIT binary patch literal 245 zcmZ3^%ge<81X(h>(lmhdV-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zw^)1~on8GjnQn;}7bWZE=BK3Q6zhS+^va783kp(GikN{4S2BDCssH7spOK%Ns$Z0! zT9lGnq+d{!pOu3<4WS)L}d}dx|NqoFs oLFF$F8=y>SPO4oI2hdnXATE{#5+9fu85wUdXk5UCir9cE0Is`09smFU literal 0 HcmV?d00001 diff --git a/src/models/MACE/__pycache__/wrapped.cpython-311.pyc b/src/models/MACE/__pycache__/wrapped.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ee4d25298c746d3001b350d08b9fec64b93921d GIT binary patch literal 6072 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 K8FV{w$o+q5 Tensor: + one_hot = self.onehot(data["z"].long()) + _, counts = torch.unique(data["batch"], return_counts=True) + _ptr = [torch.tensor([0.0], dtype=counts.dtype, device=counts.device)] + for c in counts: + _ptr.append(_ptr[-1] + c) + ptr = torch.cat(_ptr) + + data = dict( + positions=data["pos"], + edge_index=data["edge_index"], + shifts=torch.zeros( + data["edge_index"].shape[1], + 3, + device=data["pos"].device, + dtype=data["pos"].dtype, + ), + cell=torch.zeros(len(counts), 3, 3), # only for reasons of compatibility + node_attrs=one_hot, + batch=data["batch"], + ptr=ptr, + ) + + out = self.model.forward( + data, + training=True, + compute_force=False, + # disable forces computation here as it is carried out by the wrapper + ) + return out["energy"].view(-1) diff --git a/src/models/SchNet/__init__.py b/src/models/SchNet/__init__.py new file mode 100644 index 0000000..bdc9b12 --- /dev/null +++ b/src/models/SchNet/__init__.py @@ -0,0 +1 @@ +from src.models.SchNet.wrapped import SchNet diff --git a/src/models/SchNet/__pycache__/__init__.cpython-311.pyc b/src/models/SchNet/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a62350160b5f806d86fbce4d5a8ee4d847fb6d2 GIT binary patch literal 251 zcmZ3^%ge<81j$mn(sY3IV-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zx7dP{GyGCZ{4|+vNfs9+>*eOBq~;XsK}7Y+ixLY8Qd5eUfr?i$dpPZkUR+N~RtzVp6lvw~2(9_cgny3#kQ6FNWetdjpUS>&r qyk0@&FAf`^Txm|KT@eS+U`8M=mIV?Ym>C%vZ!qXwz=n$0fGPkX21CpM literal 0 HcmV?d00001 diff --git a/src/models/SchNet/__pycache__/wrapped.cpython-311.pyc b/src/models/SchNet/__pycache__/wrapped.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5d64fd8210d7d5c638aee851a618a6560f21d21 GIT binary patch literal 2106 zcma(SOKclO^zGNXb{yM~g#6RAKeGn=?w@0!^)CCx_d zp%N~t1VTb72#Fj}0Hp_zIB?*=fdj|1s?^mS5E7@}HcBg@Uf|6-cAB!+Nh1sa{;y3>gQ*X5<9CLz?foF&`$sTcBs-)UKH(~Bqz zd08+`A8Ff(nt>nkqL3`woiJ?L$c`+!zUx{TNMu_cwVB`a7x}OgwA!7BT5iA`8al>9 zL7GSxaPItp;B#pWqK6jGNquHr?o$urSdNugjkQ?1A#LF0ZT2q^R?R54r@5yW>v8Ue zyn$<*gZ0&XB)r%i8;Cdi46F|<4n#=Ad92TTi znK+-Tr0GOl3p==v8x~$*GqNnMwM4=7&K}}wNIf@Wp6JHF1FoOHV6(-LjR~L1!oF!) zj%SCVWrZS|1AF80gOl|o^ry}O$_QokHVe*CCkpFM;JeKB&(}kTHQOkt)#@R0>X}br zy;qj?3(Rh}DXF!W*(h4RSnA1Z{TM_p!!Hp0eHreLLFK^h^1(ah>aB7$nSLP|Ka|WI zOC}E|Pfw%;s2tq}y^_BtKv%n8hN-znl#=62d@?qTsL;jV4mu}p0Q;Qlj7A;iJA;zJB~dQo zrP%of!5YM{j`;BrOGKI&h}Q>gF%TiKzi@Gfyte~6Wskd|?th_H@G*U9>eUp2q>zw~d; z)^5+%et7fe(VyP_HTwPJZ>yh9-mILtT{+X$lhWuXD<7|1KYsPnwM$#ZESo|bRr$n; zYC$xGkL`p_k+&>7t>X!L$gc9q?%xKn4M2^ zf-+!oCN%|>hq;4kK~O_5J(m^*H4KLirzJs^VP<~MP-g8>@RlTNbL-EhfatrIGxR?| zmbom3s=>AF+397Vg3<7iN8X{DsQoNWyo4QI>veeTEdJv$@wjG&zK5PsQ3Vf-S9(|Q zDm#v>$X&RCAXOwuN??3z|ChknR{u-jxvl-* Tensor: + energy = self.model(z=data["z"].long(), pos=data["pos"], batch=data["batch"]) + return energy.view(-1) diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/base.py b/src/models/base.py new file mode 100644 index 0000000..6763e93 --- /dev/null +++ b/src/models/base.py @@ -0,0 +1,91 @@ +import torch +from torch_geometric.data import Data +from torch import Tensor +from typing import Optional, List, Dict, Tuple +from src.models.reference_energy import ReferenceEnergies + + +# for correct type conversions +def gradient( + y: torch.Tensor, x: List[torch.Tensor], create_graph: bool, retain_graph: bool +) -> List[torch.Tensor]: + grad_outputs: List[Optional[torch.Tensor]] = [torch.ones_like(y)] + grads = torch.autograd.grad( + [y], + x, + grad_outputs=grad_outputs, + create_graph=create_graph, + retain_graph=retain_graph, + allow_unused=True, + ) + new_grads = [] + for grad in grads: + if grad is not None: + new_grads.append(grad) + else: + raise Exception("grad was None") + return new_grads + + +class BaseModel(torch.nn.Module): + """ + Base class for implementing new models or wrapping existing models. + """ + + def __init__( + self, + hyperparameters: Dict, + compute_forces: bool = False, + ): + """ + :param hyperparameters: Dictionary storing hyperparameters that are needed for saving and restoring the model + :param compute_forces: Boolean that indicates if forces (negative gradients wrt energy) should be computed; + default: False + """ + super().__init__() + self.hyperparameters = hyperparameters + self.compute_forces = compute_forces + self.atom_ref_model = ReferenceEnergies(freeze=True) + + def set_constant_energies( + self, energy_dict: Dict[int, float], freeze: bool = True + ) -> None: + """ + :param energy_dict: Dictionary mapping atomic numbers to atomwise energy contributions + :param freeze: Boolean, whether atomwise energies should be frozen during training; default: True + :return: None + """ + self.atom_ref_model.set_constant_energies( + energy_dict=energy_dict, freeze=freeze + ) + + def model_forward(self, data: Data) -> Tensor: + """ + Child classes should implement the forward pass for computing the energy here. + :param data: Data object storing information that is need for the model + :return: Tensor with energy prediction + """ + raise NotImplementedError + + def forward(self, data: Data) -> Tuple[Tensor, Optional[Tensor]]: + torch.set_grad_enabled(True) + + forces = None + if self.compute_forces: + data["pos"].requires_grad_() + + energy = self.model_forward(data).view(-1) + + if self.compute_forces: + grads = gradient( + energy, + [data["pos"]], + create_graph=self.training, + retain_graph=self.training, + ) + + forces = (-1.0) * grads[0] + + atom_energies = self.atom_ref_model(data["z"], data["batch"]) + energy += atom_energies + return energy, forces diff --git a/src/models/reference_energy.py b/src/models/reference_energy.py new file mode 100644 index 0000000..02ec4f0 --- /dev/null +++ b/src/models/reference_energy.py @@ -0,0 +1,143 @@ +import torch +from torch import Tensor +from torch_geometric.loader import DataLoader +from tqdm import tqdm +from torch_scatter import scatter_add +from src.data import ConfRankDataset + + +class ReferenceEnergies(torch.nn.Module): + """ + PyTorch module for fitting the reference energies (per species) + """ + + def __init__(self, freeze: bool = True, num_embeddings: int = 95) -> None: + """ + :param freeze: Freeze module weights after init, default: True -> No optimization during training + :param num_embeddings: Number of expected elements, default: default + """ + super(ReferenceEnergies, self).__init__() + # constant energy shifts + self.constant_shifts = torch.nn.Parameter( + torch.zeros(num_embeddings, 1), requires_grad=bool(~freeze) + ) + + def loss_fn(self, prediction: Tensor, target: Tensor) -> Tensor: + """ + Loss function that is minimized during the fit: MSE loss + :param prediction: Tensor storing predicted energies + :param target: Tensor with actual energies + :return: scalar loss Tensor + """ + out = torch.mean(torch.square(prediction - target)) + return out + + def fit_constant_energies( + self, + trainset: ConfRankDataset, + target_key: str = "energy", + epochs: int = 10, + batch_size: int = 50, + lr: float = 1e-3, + freeze_after: bool = True, + ) -> None: + """ + Fit the energies against number of atom species to obtain coefficients for the constant model using SGD. + For small dataset it is recommended to perform a fit with sklearn etc. + + :param trainset: Dataset + :param target_key: Key for target of regression; default: energy + :param epochs: Number of epochs for optimization, default: 10 + :param batch_size: Size of mini batches, default: 50 + :param lr: Learning rate, default: 1e-3 + :param freeze_after: Whether the weights/reference energies should be frozen after this fit + :return: None + """ + initial_device = self.constant_shifts.device + device = torch.device("cpu") + train_loader = DataLoader( + trainset, shuffle=True, drop_last=True, batch_size=batch_size + ) + # activate gradient computation for constant shifts + self.constant_shifts.requires_grad_(True) + self.to(device) + optim = torch.optim.Adam(lr=lr, params=[self.constant_shifts]) + tqdm_loader_outer = tqdm(range(epochs), desc="Epoch") + for epoch in tqdm_loader_outer: + tqdm_loader_outer.set_description( + f"Epoch {epoch + 1}/{len(tqdm_loader_outer)}" + ) + summed_loss = 0 + summed_rmse = 0 + tqdm_loader_inner = tqdm(train_loader, leave=False, position=1) + for step, data in enumerate(tqdm_loader_inner): + tqdm_loader_inner.set_description( + f"Step {step + 1}/{len(tqdm_loader_inner)}" + ) + batch = data["batch"].to(device) + energy = data[target_key].to(device) + species = data["z"].long().to(device) + + energy_shifts = self.constant_shifts[..., 0][species] + prediction = scatter_add( + energy_shifts, + index=batch, + dim=0, + dim_size=len(torch.unique(batch)), + ) + loss = self.loss_fn(energy, prediction) + rmse = torch.sqrt(torch.mean(torch.square(prediction - energy))) + summed_rmse += rmse.item() + optim.zero_grad() + loss.backward() + optim.step() + summed_loss += loss.item() + tqdm_loader_inner.set_postfix_str( + f"step loss: {loss.item():.4E}, step rmse: {rmse.item():.4E}" + ) + tqdm_loader_outer.set_postfix_str( + f"Avg. loss: {summed_loss / len(tqdm_loader_inner):.4E}, " + f"Avg. rmse: {summed_rmse / len(tqdm_loader_inner):.4E}" + ) + self.constant_shifts.requires_grad_(freeze_after) + self.constant_shifts.to(initial_device) + print("Finished fitting the per-atom-energies.") + + def get_constant_energies( + self, + ) -> dict[int, float]: + output_dict = {} + for z, el in enumerate(self.constant_shifts): + if el.abs() > 1e-10: + output_dict[z] = el.item() + return output_dict + + def set_constant_energies( + self, energy_dict: dict[int, float], freeze: bool = True + ) -> None: + """ + :param energy_dict: Mapping of {atomic_number : reference_energy} + :param freeze: Should the model weights be frozen after setting the energies?, default: True + :return: None + """ + self.constant_shifts.requires_grad_(False) + for z in energy_dict.keys(): + self.constant_shifts[z] = energy_dict[z] + self.constant_shifts.requires_grad_(freeze) + + def forward( + self, + species: Tensor, + batch: Tensor, + ) -> Tensor: + """ + :param species: 1D Tensor with atomic numbers of shape (N,) + :param batch: 1D Tensor with batch indices of shape (N,) + :return: Contribution from summing up reference energies + """ + + energy_shift_per_atom = self.constant_shifts[..., 0][species.long()] + constant_contribution = scatter_add( + energy_shift_per_atom, index=batch, dim=0, dim_size=len(torch.unique(batch)) + ) + return constant_contribution diff --git a/src/training/lightning.py b/src/training/lightning.py new file mode 100644 index 0000000..6e6a891 --- /dev/null +++ b/src/training/lightning.py @@ -0,0 +1,477 @@ +import torch +from torch import Tensor +import pytorch_lightning as pl +from typing import Optional, Callable +import matplotlib.pyplot as plt +import seaborn as sns +from src.training.metrics import ( + MAE, + MAX_AE, + Stddev_AE, + MAD, + RMSE, + R2Score, + SignFlipPercentage, + ActualVsPredicted, + RankingMetrics, +) + +sns.set_theme() + +# stores tuples (bool, metric), where the bool indicates whether metric is used in force computations as well +metric_dict = { + "MAE": (True, MAE), + "MAX_AE": (True, MAX_AE), + "Stddev_AE": (True, Stddev_AE), + "MAD": (True, MAD), + "RMSE": (True, RMSE), + "R2Score": (False, R2Score), + "SignFlipPctg": (False, SignFlipPercentage), +} + + +def huber_loss(x, y): + return torch.nn.functional.huber_loss(x, y, delta=0.01) + + +class LightningWrapper(pl.LightningModule): + def __init__( + self, + model: torch.nn.Module, + lr: float = 1e-3, + weight_decay: float = 0.0, + decay_patience: int = 10, + decay_factor: float = 0.5, + min_lr: float = 1e-5, + energy_key: str = "energy", + forces_key: Optional[str] = None, + atomic_numbers_key: str = "z", + energy_tradeoff: float = 1.0, + forces_tradeoff: Optional[float] = 1.0, + energy_loss_fn: Callable = huber_loss, + forces_loss_fn: Callable = huber_loss, + xy_lim=None, + pairwise=True, + ): + """ + :param model: Model that should be trained + :param lr: learning rate, default: 1e-3 + :param weight_decay: l2 regularization, default=0.0 + :param decay_patience: patience when using certain schedulers, e.g. cosine decay (optional), default: None + :param decay_factor: lr decay factor when using certain schedulers, e.g. cosine decay (optional), default: None + :param min_lr: minimal learning rate when using certain schedulers, e.g. cosine decay (optional), default: None + :param energy_key: Allows for custom energy key, default: 'energy' + :param forces_key: Allows for custom force key (optional), default: 'forces' + :param energy_tradeoff: Tradeoff in loss for energy, default: 1.0 + :param forces_tradeoff: Tradeoff in loss for forces (optional), default: 1.0 + :param energy_loss_fn: Loss function for energy contributions, default: huber loss with delta=0.01 + :param forces_loss_fn: Loss function for force contributions, default: huber loss with delta=0.01 + :param xy_lim: limit for x- and y-axis in the "actual vs. predicted" plot + :param pairwise: boolean, changes training/val/test to single or pairwise mode. + Need to change the dataloaders accordingly + """ + super().__init__() + self.model = model + self.lr = lr + self.min_lr = min_lr + self.weight_decay = weight_decay + self.decay_patience = decay_patience + self.decay_factor = decay_factor + self.energy_key = energy_key + self.forces_key = forces_key + self.energy_loss_fn = energy_loss_fn + self.forces_loss_fn = forces_loss_fn + self.atomic_numbers_key = atomic_numbers_key + self.xy_lim = xy_lim + self.pairwise = pairwise + # define output properties for training + self.output_properties = dict(single=["energy"], pairwise=["energy_difference"]) + self.property_tradeoffs = { + "energy": energy_tradeoff, + "energy_difference": energy_tradeoff, + "forces": forces_tradeoff, + } + # case handling for training with forces + if forces_key is not None and forces_tradeoff > 0.0: + compute_forces = True + else: + compute_forces = False + self.compute_forces(mode=compute_forces) + self.reset_metrics() + self.model.train() + + def compute_forces(self, mode: bool = True): + self._compute_forces = mode + self.model.compute_forces = mode + if mode: + for m in ["single", "pairwise"]: + if "forces" not in self.output_properties[m]: + self.output_properties[m].append("forces") + # need to reset metrics if compute_forces is changed after training + # otherwise there would be missing or invalid keys in self.metrics during evaluation or testing + self.reset_metrics() + + def reset_metrics(self): + # specify metrics + metrics = {} + for mode in ["single", "pairwise"]: + subset_metrics = {} + for subset in ["train", "val", "test"]: + prop_metrics = {} + for prop in self.output_properties[mode]: + metric_metrics = {} + for metric_name, bool_metric in metric_dict.items(): + use_for_forces, metric = bool_metric + if prop == "forces" and not use_for_forces: + continue + else: + metric_metrics[metric_name] = metric( + forces=(prop == "forces") + ) + prop_metrics[prop] = metric_metrics + subset_metrics[subset] = prop_metrics + metrics[mode] = subset_metrics + + self.energy_actual_vs_predicted = { + subset: ActualVsPredicted(xy_lim=self.xy_lim) + for subset in ["train", "val", "test"] + } + + self.ranking_metrics = { + subset: RankingMetrics() for subset in ["train", "val", "test"] + } + + self.metrics = metrics + + def forward(self, data): + if self.pairwise: + data_1, data_2 = data + out = self.forward_pairwise(data_1, data_2) + else: + out = self.forward_single(data) + return out + + def forward_single(self, data): + energy, forces = self.model(data) + return energy, forces + + def forward_pairwise(self, data_1, data_2): + # rename keys of cartesian coordinates and atomic numbers: + if "z" not in data_1.keys(): + temp = dict(z=data_1[self.atomic_numbers_key]) + data_1.update(temp) + temp = dict(z=data_2[self.atomic_numbers_key]) + data_2.update(temp) + else: + temp = dict(z=data_1["z"].long()) + data_1.update(temp) + temp = dict(z=data_2["z"].long()) + data_2.update(temp) + + energy_1, forces_1 = self.model(data_1) + energy_2, forces_2 = self.model(data_2) + energy_difference = energy_1 - energy_2 + forces = ( + torch.cat([forces_1, forces_2], dim=0) if self._compute_forces else None + ) # concatenate force components + return energy_difference, forces + + def loss_single(self, prediction, target) -> Tensor: + loss = torch.tensor( + 0.0, + device=target["energy"].device, + dtype=target["energy"].dtype, + ) + for prop, val in prediction.items(): + if prop == "energy": + contrib = self.energy_loss_fn(prediction[prop], target["energy"]) + elif prop == "forces" and val is not None: + contrib = self.forces_loss_fn(prediction[prop], target["forces"]) + else: + continue + loss += self.property_tradeoffs[prop] * contrib + return loss + + def loss_pairwise(self, prediction, target) -> Tensor: + loss = torch.tensor( + 0.0, + device=target["energy_difference"].device, + dtype=target["energy_difference"].dtype, + ) + for prop, val in prediction.items(): + if prop == "energy_difference": + contrib = self.energy_loss_fn( + prediction[prop], target["energy_difference"] + ) + elif prop == "forces" and val is not None: + contrib = self.forces_loss_fn(prediction[prop], target["forces"]) + else: + continue + loss += self.property_tradeoffs[prop] * contrib + return loss + + def create_regression_target_pairwise(self, data_1, data_2): + energy_difference_target = data_1[self.energy_key] - data_2[self.energy_key] + assert data_1["ensbid"] == data_2["ensbid"] + target = dict( + energy_difference=energy_difference_target, + ensbid=data_1.ensbid, + confid_1=data_1["confid"], + confid_2=data_2["confid"], + ) + + if self._compute_forces: + forces = torch.cat( + [data_1[self.forces_key], data_2[self.forces_key]], dim=0 + ) + target["forces"] = forces + + return target + + def training_step( + self, + data, + batch_idx: Tensor, + ) -> Tensor: + if self.pairwise: + data_1, data_2 = data + energy_difference, forces = self.forward_pairwise(data_1, data_2) + prediction = dict(energy_difference=energy_difference, forces=forces) + target = self.create_regression_target_pairwise(data_1, data_2) + loss = self.loss_pairwise(prediction, target) + self.log( + f"ptl/train_loss_pairwise", + loss, + batch_size=1, + on_step=True, + on_epoch=True, + prog_bar=True, + ) + return loss + else: + energy, forces = self.forward_single(data) + prediction = dict(energy=energy, forces=forces) + target = dict(energy=data[self.energy_key]) + if self._compute_forces: + assert forces is not None + target["forces"] = data["forces"] + loss = self.loss_single(prediction, target) + self.log( + f"ptl/train_loss_single", + loss, + batch_size=1, + on_step=True, + on_epoch=True, + prog_bar=True, + ) + return loss + + def validation_step( + self, + data, + batch_idx: Tensor, + dataloader_idx: int = 0, + ) -> None: + if self.pairwise: + assert len(data) == 2 + data_1, data_2 = data + self._eval_at_step_pairwise( + data_1, data_2, subset="val", dataloader_idx=dataloader_idx + ) + else: + self._eval_at_step_single(data, subset="val", dataloader_idx=dataloader_idx) + + def test_step( + self, + data, + batch_idx: Tensor, + dataloader_idx: int = 0, + ): + if self.pairwise: + assert len(data) == 2 + data_1, data_2 = data + self._eval_at_step_pairwise( + data_1, data_2, subset="test", dataloader_idx=dataloader_idx + ) + else: + self._eval_at_step_single( + data, subset="test", dataloader_idx=dataloader_idx + ) + + def on_validation_epoch_end(self) -> None: + self._aggregate_metrics("val", self.pairwise) + + def on_test_epoch_end(self) -> None: + self._aggregate_metrics("test", self.pairwise) + + def _eval_at_step_single( + self, + data, + subset: str, + dataloader_idx: int, + ) -> None: + """ + :param data: mini-batch + :param subset: either 'train', 'val' or 'test' + :return: None + """ + + energy, forces = self.forward_single(data) + prediction = dict( + energy=energy.detach().cpu(), + forces=forces.detach().cpu() if forces is not None else None, + ) + target = dict(energy=data[self.energy_key].cpu()) + if self._compute_forces: + assert forces is not None + target["forces"] = data["forces"].cpu() + loss = self.loss_single(prediction, target) + self.log( + f"ptl/{subset}_loss_single", + loss, + batch_size=1, + on_step=False, + on_epoch=True, + prog_bar=True, + ) + for prop, metrics in self.metrics["single"][subset].items(): + pred_for_prop = prediction[prop].contiguous() + target_for_prop = target[prop].contiguous() + for metric_name, metric in metrics.items(): + _ = metric(pred_for_prop, target_for_prop) + + def _eval_at_step_pairwise( + self, + data_1, + data_2, + subset: str, + dataloader_idx: int, + ) -> None: + """ + :param data: mini-batch + :param subset: either 'train', 'val' or 'test' + :return: None + """ + + energy_difference, forces = self.forward_pairwise(data_1, data_2) + prediction = dict( + energy_difference=energy_difference.detach().cpu(), + forces=forces.detach().cpu() if forces is not None else None, + ) + target = { + key: (val.cpu() if isinstance(val, torch.Tensor) else val) + for key, val in self.create_regression_target_pairwise( + data_1, data_2 + ).items() + } + + loss = self.loss_pairwise(prediction, target) + self.log( + f"ptl/{subset}_loss_pairwise", + loss, + batch_size=1, + on_step=False, + on_epoch=True, + prog_bar=True, + ) + for prop, metrics in self.metrics["pairwise"][subset].items(): + pred_for_prop = prediction[prop].contiguous() + target_for_prop = target[prop].contiguous() + for metric_name, metric in metrics.items(): + _ = metric(pred_for_prop, target_for_prop) + + if prop == "energy_difference": + self.energy_actual_vs_predicted[subset].update( + preds=pred_for_prop, target=target_for_prop + ) + self.ranking_metrics[subset].update(target=target, pred=prediction) + + def _aggregate_metrics(self, subset: str, pairwise: bool) -> None: + """ + Aggregate and then reset all metrics + :param subset: either 'train', 'val' or 'test' + :return: None + """ + suffix = self.metrics_suffix(pairwise) + # store the computed metrics in addition to logging them + temp_metrics = {} + + subset_metrics = self.metrics[suffix][subset] + for prop, prop_metrics in subset_metrics.items(): + for metric_name, metric in prop_metrics.items(): + metric_key = "ptl/" + "_".join([prop, subset, metric_name]) + metric_val = metric.compute() + self.log( + "_".join([metric_key, suffix]), + metric_val, + on_epoch=True, + on_step=False, + prog_bar="MAE" in metric_key, + batch_size=1, + ) + metric.reset() + temp_metrics[metric_key] = metric_val + + # additional metrics in case of pairwise mode + if pairwise: + assert "energy_difference" in subset_metrics.keys() + # top k scores: + ranking_metrics = self.ranking_metrics[subset].compute() + self.ranking_metrics[subset].reset() + for key, val in ranking_metrics.items(): + self.log( + f"ptl/{key}_{subset}", + val, + on_epoch=True, + on_step=False, + prog_bar=subset == "val", + batch_size=1, + ) + + # actual vs predicted plot + fig = self.energy_actual_vs_predicted[subset].compute( + R2_score=temp_metrics[f"ptl/energy_difference_{subset}_R2Score"], + RMSE=temp_metrics[f"ptl/energy_difference_{subset}_RMSE"], + MAE=temp_metrics[f"ptl/energy_difference_{subset}_MAE"], + SignFlipPercentage=temp_metrics[ + f"ptl/energy_difference_{subset}_SignFlipPctg" + ], + Top1Score=ranking_metrics["top_1"], + Top3Score=ranking_metrics["top_3"], + Top5Score=ranking_metrics["top_5"], + spearman_ensemble_mean=ranking_metrics["spearman_ensemble_mean"], + pearson_ensemble_mean=ranking_metrics["pearson_ensemble_mean"], + kendall_tau_ensemble_mean=ranking_metrics["kendall_tau_ensemble_mean"], + ) + plt.close() + self.logger.experiment.log_figure( + self.logger.run_id, + fig, + artifact_file=f"epoch={self.current_epoch:03d}_actual_vs_predicted_{subset}.png", + ) + self.energy_actual_vs_predicted[subset].reset() + + def configure_optimizers(self): + opt = torch.optim.Adam( + self.parameters(), lr=self.lr, weight_decay=self.weight_decay + ) + + _scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer=opt, + mode="min", + min_lr=self.min_lr, + patience=self.decay_patience, + factor=self.decay_factor, + ) + + scheduler = { + "scheduler": _scheduler, + "monitor": f"ptl/val_loss_{self.metrics_suffix(self.pairwise)}", + "name": "lr_scheduler", + } + schedulers = [scheduler] + + return [opt], schedulers + + def metrics_suffix(self, pairwise: bool) -> str: + return "pairwise" if pairwise else "single" diff --git a/src/training/metrics.py b/src/training/metrics.py new file mode 100644 index 0000000..02c05fc --- /dev/null +++ b/src/training/metrics.py @@ -0,0 +1,335 @@ +from typing import Optional +import numpy as np +import torch +from torch import Tensor +import torchmetrics +from torchmetrics.utilities import dim_zero_cat +from scipy.stats import spearmanr, pearsonr, kendalltau, rankdata +import matplotlib.pyplot as plt +from matplotlib import gridspec +import seaborn as sns +from matplotlib.colors import LogNorm + + +class MAE(torchmetrics.MeanAbsoluteError): + def __init__(self, **kwargs): + super().__init__() + + def update(self, pred, target): + super().update(pred, target) + + +class RMSE(torchmetrics.MeanSquaredError): + def __init__(self, **kwargs): + super(RMSE, self).__init__(squared=False) + + def update(self, pred, target): + super().update(pred, target) + + +class MAX_AE(torchmetrics.MaxMetric): + def __init__(self, **kwargs): + super(MAX_AE, self).__init__() + + def update(self, pred, target): + abs = (pred - target).abs() + super().update(abs) + + +class R2Score(torchmetrics.R2Score): + def __init__(self, **kwargs): + super().__init__() + + def update(self, pred, target): + if len(pred.shape) > 1: + if pred.shape[1] > 1: + # in case of forces -> Compute norm first + pred = torch.norm(pred, p=2, dim=-1) + target = torch.norm(target, p=2, dim=-1) + super().update(pred.view(-1), target.view(-1)) + + +class ActualVsPredicted: + def __init__(self, xy_lim: Optional[float] = None): + self.preds = [] + self.target = [] + self.xy_lim = xy_lim + self.epoch = 0 + + def update(self, preds: Tensor, target: Tensor) -> None: + self.preds.append(preds.detach().cpu()) + self.target.append(target.detach().cpu()) + + def compute( + self, + R2_score, + RMSE, + MAE, + SignFlipPercentage, + Top1Score, + Top3Score, + Top5Score, + spearman_ensemble_mean, + pearson_ensemble_mean, + kendall_tau_ensemble_mean, + ): + f = plt.figure(figsize=(8, 8)) + gs = gridspec.GridSpec(4, 4) + + ax_main = plt.subplot(gs[1:4, 0:3]) + ax_x_dist = plt.subplot(gs[0, 0:3], sharex=ax_main) + ax_y_dist = plt.subplot(gs[1:4, 3], sharey=ax_main) + ax_metrics = plt.subplot(gs[0, 3]) # Additional subplot for metrics + + preds = torch.cat(self.preds) + target = torch.cat(self.target) + + xy_lim = ( + self.xy_lim + if self.xy_lim is not None + else torch.max(torch.abs(torch.cat([preds, target]))).item() * 1.10 + ) + + # Generate a heatmap instead of a scatterplot + heatmap_data, xedges, yedges = np.histogram2d( + preds.numpy(), + target.numpy(), + bins=150, + range=[[-xy_lim, xy_lim], [-xy_lim, xy_lim]], + ) + extent = (xedges[0], xedges[-1], yedges[0], yedges[-1]) + ax_main.imshow( + heatmap_data.T, + extent=extent, + origin="lower", + aspect="auto", + cmap="viridis", + norm=LogNorm(), + ) + ax_main.plot([0, 1], [0, 1], transform=ax_main.transAxes, color="red") + + # Plotting the density on the separate axes + sns.kdeplot(x=preds.numpy(), ax=ax_x_dist, color="blue", fill=True) + sns.kdeplot(y=target.numpy(), ax=ax_y_dist, color="blue", fill=True) + + # Hide the density plot labels + ax_x_dist.tick_params(axis="x", labelbottom=False) + ax_y_dist.tick_params(axis="y", labelleft=False) + + # Set the limits on the density axes + ax_x_dist.set_xlim(-xy_lim, xy_lim) + ax_y_dist.set_ylim(-xy_lim, xy_lim) + + ax_main.set_xlabel("$E_{ML}^{(1)} - E_{ML}^{(2)}$") + ax_main.set_ylabel("$E_{DFT}^{(1)} - E_{DFT}^{(2)}$") + ax_main.set_xlim(-xy_lim, xy_lim) + ax_main.set_ylim(-xy_lim, xy_lim) + ax_main.set_title(f"epoch: {self.epoch:03d}") + # Add metrics to the new subplot + metrics_text = ( + f"MAE: {MAE:.2E}\n" + f"RMSE: {RMSE:.2E}\n" + f"R2 score: {R2_score:.2f}\n" + f"SignFlipPctg: {SignFlipPercentage:.3f}\n" + f"Top-1 score: {Top1Score:.3f}\n" + f"Top-3 score: {Top3Score:.3f}\n" + f"Top-5 score: {Top5Score:.3f}\n" + f"spearman_ensbl_mean: {spearman_ensemble_mean:.3f}\n" + f"pearson_ensbl_mean: {pearson_ensemble_mean:.3f}\n" + f"kendall_tau_ensbl_mean: {kendall_tau_ensemble_mean:.3f}\n" + ) + ax_metrics.text( + 0.5, + 0.5, + metrics_text, + ha="center", + va="center", + fontsize=9, + transform=ax_metrics.transAxes, + ) + ax_metrics.axis("off") # Turn off the axis + plt.tight_layout() + + self.epoch += 1 + return f + + def reset(self): + self.preds = [] + self.target = [] + + +class SignFlipPercentage(torchmetrics.Metric): + is_differentiable: Optional[bool] = False + higher_is_better: Optional[bool] = False + + def __init__(self, **kwargs): + super(SignFlipPercentage, self).__init__(compute_on_cpu=True) + self.add_state("q1_count", default=torch.tensor(0), dist_reduce_fx="sum") + self.add_state("q3_count", default=torch.tensor(0), dist_reduce_fx="sum") + self.add_state("total_count", default=torch.tensor(0), dist_reduce_fx="sum") + + def update(self, preds: Tensor, target: Tensor) -> None: + self.q1_count += torch.sum((preds > 0.0) & (target < 0.0)).long() + self.q3_count += torch.sum((preds < 0.0) & (target > 0.0)).long() + self.total_count += len(preds) + + def compute(self): + percentage = ( + ((self.q1_count + self.q3_count) / self.total_count) + if self.total_count > 0 + else 0 + ) + return percentage + + +class Stddev_AE(torchmetrics.Metric): + is_differentiable: Optional[bool] = True + higher_is_better: Optional[bool] = False + + def __init__(self, **kwargs): + super().__init__(compute_on_cpu=True) + self.add_state("diffs", default=[], dist_reduce_fx="cat") + + def update(self, preds: Tensor, target: Tensor) -> None: + self.diffs.append(preds - target) + + def compute(self): + diffs = dim_zero_cat(self.diffs) + std = torch.std(torch.abs(diffs)) + return std + + +class MAD(torchmetrics.Metric): + is_differentiable: Optional[bool] = True + higher_is_better: Optional[bool] = False + + def __init__(self, forces=False, **kwargs): + super().__init__(compute_on_cpu=True) + self.add_state("preds", default=[], dist_reduce_fx="cat") + self.add_state("target", default=[], dist_reduce_fx="cat") + self.forces = forces + + def update(self, preds: Tensor, target: Tensor) -> None: + self.preds.append(preds) + self.target.append(target) + + def compute(self): + preds = dim_zero_cat(self.preds) + target = dim_zero_cat(self.target) + if self.forces: + dataset_mean = torch.mean(preds, dim=1, keepdim=True) + mad = torch.mean(torch.abs(target - dataset_mean)) + else: + dataset_mean = torch.mean(preds) + mad = torch.mean(torch.abs(target - dataset_mean)) + return mad + + +class RankingMetrics: + def __init__(self): + # ks for top-k errors + self.ks = [1, 3, 5] + self.ensemble_values = {"target": {}, "pred": {}} + + @staticmethod + def get_longest_row(ensemble_values): + result = {} + # iterate over all ensembles + for ensbid in ensemble_values.keys(): + longest_row = [] + # each element in ensemble_values[ensbid] stores a row of the upper triangle matrix of pairwise differences + # look for the longest row, i.e., the row that stores information about ALL pairwise differences + for confid in ensemble_values[ensbid].keys(): + if len(ensemble_values[ensbid][confid]) >= len(longest_row): + longest_row = ensemble_values[ensbid][confid] + longest_row.append( + [ + confid, + 0.0, + ] + ) # add distance to itself (diagonal element of pairwise distance matrix) for completeness + result[ensbid] = longest_row + return result + + def update(self, target, pred): + ensbids = target["ensbid"] + confids_1 = target["confid_1"] + confids_2 = target["confid_2"] + for key, data in zip(["target", "pred"], [target, pred]): + diffs = data["energy_difference"] + # store pairwise differences for conf1 in each ensemble + # amounts to row-wise saving of the upper triangle matrix of pairwise differences for each ensemble + # instead of saving the actual matrix (or its rows) we save tuples in a dictionary, + # so extraction by conformer_id is easier and such that we do not rely on a particluar order of conformer ids + for diff, ensbid, confid_1, confid_2 in zip( + diffs, ensbids, confids_1, confids_2 + ): + # create dict for ensemble if it does not exist already + if ensbid not in self.ensemble_values[key].keys(): + self.ensemble_values[key][ensbid] = {} + # create row of upper triangle pairwise difference matrix if it does not exist + if confid_2 not in self.ensemble_values[key][ensbid].keys(): + # print(confid_2) + self.ensemble_values[key][ensbid][confid_2] = [] + # save + self.ensemble_values[key][ensbid][confid_2].append( + [confid_1, diff.item()] + ) + + def compute(self): + pairwise_diffs_ref = self.get_longest_row(self.ensemble_values["target"]) + pairwise_diffs = self.get_longest_row(self.ensemble_values["pred"]) + correct = { + k: 0 for k in self.ks + } # found the actual lowest conformer in the k lowest predictions + spearman_correlation_coeffs = [] + pearson_correlation_coeffs = [] + kendall_tau_dists = [] + total = 0 + for ensbid in pairwise_diffs_ref.keys(): + # relative energies with respect to confid that corresponds to row no. 0 in the pairwise difference matrix + target_rel_energies = [p[1] for p in pairwise_diffs_ref[ensbid]] + predicted_rel_energies = [p[1] for p in pairwise_diffs[ensbid]] + spearman_coff = spearmanr(target_rel_energies, predicted_rel_energies)[0] + spearman_correlation_coeffs.append(spearman_coff) + pearson_coff = pearsonr(target_rel_energies, predicted_rel_energies)[0] + pearson_correlation_coeffs.append(pearson_coff) + kendall_tau_val = kendalltau( + rankdata(target_rel_energies), + rankdata(predicted_rel_energies), + )[0] + kendall_tau_dists.append(kendall_tau_val) + + # sort conformer ids for computing top-k accuracies + sorted_target_confs = [ + p[0] for p in sorted(pairwise_diffs_ref[ensbid], key=lambda x: x[1]) + ] + sorted_predicted_confs = [ + p[0] for p in sorted(pairwise_diffs[ensbid], key=lambda x: x[1]) + ] + for k in self.ks: + if sorted_target_confs[0] in sorted_predicted_confs[:k]: + correct[k] += 1 + total += 1 + results = dict( + spearman_ensemble_mean=np.mean(spearman_correlation_coeffs).item(), + spearman_ensemble_stddev=np.std(spearman_correlation_coeffs).item(), + spearman_ensemble_median=np.median(spearman_correlation_coeffs).item(), + spearman_ensemble_max=np.max(spearman_correlation_coeffs).item(), + spearman_ensemble_min=np.min(spearman_correlation_coeffs).item(), + pearson_ensemble_mean=np.mean(pearson_correlation_coeffs).item(), + pearson_ensemble_stddev=np.std(pearson_correlation_coeffs).item(), + pearson_ensemble_median=np.median(pearson_correlation_coeffs).item(), + pearson_ensemble_max=np.max(pearson_correlation_coeffs).item(), + pearson_ensemble_min=np.min(pearson_correlation_coeffs).item(), + kendall_tau_ensemble_mean=np.mean(kendall_tau_dists).item(), + kendall_tau_ensemble_stddev=np.std(kendall_tau_dists).item(), + kendall_tau_ensemble_median=np.median(kendall_tau_dists).item(), + kendall_tau_ensemble_max=np.max(kendall_tau_dists).item(), + kendall_tau_ensemble_min=np.min(kendall_tau_dists).item(), + ) + top_k_accuracies = {f"top_{k}": c / total for k, c in correct.items()} + return {**results, **top_k_accuracies} + + def reset(self): + self.ensemble_values = {"target": {}, "pred": {}} diff --git a/src/transform/__init__.py b/src/transform/__init__.py new file mode 100644 index 0000000..529abd0 --- /dev/null +++ b/src/transform/__init__.py @@ -0,0 +1,8 @@ +from .base_processor import BaseProcessor, DummyProcessor +from .base_transform import BaseTransform, DummyTransform +from .filter_rmsd import FilterRmsd +from .graph_construction import RadiusGraph +from .pipeline import PipelineTransform +from .rename import Rename +from .scale import Scale +from .units_conversion import ConvertUnitsToSI diff --git a/src/transform/base_processor.py b/src/transform/base_processor.py new file mode 100644 index 0000000..f4289ee --- /dev/null +++ b/src/transform/base_processor.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + +from torch_geometric.data import InMemoryDataset + + +class BaseProcessor(ABC): + """Base class for processing applied to a `ConfRankDataset`. + NOTE: Processor are applied on a dataset object and return a dataset object + + Example: + >> proc = Dummy_Processor() + >> ds = ConfRankDataset(...) + >> ds_new = proc(ds) + """ + + @abstractmethod + def __call__(self, dataset: InMemoryDataset) -> InMemoryDataset: + """Apply transformation to `InMemoryDataset` object and return transformed `InMemoryDataset` object.""" + pass + + +class DummyProcessor(BaseProcessor): + """Dummy Processor for testing purposes.""" + + def __call__(self, dataset: InMemoryDataset) -> InMemoryDataset: + return dataset[:2] diff --git a/src/transform/base_transform.py b/src/transform/base_transform.py new file mode 100644 index 0000000..cf5ee94 --- /dev/null +++ b/src/transform/base_transform.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +from torch_geometric.data import Data + + +class BaseTransform(ABC): + """Base class for transformations applied to a `ConfRankDataset`. + NOTE: Transformations are only lazily applied, when sample is accessed. + + Example: + >> trafo = Dummy_Transform() + >> ds = ConfRankDataset(..., transform=trafo) + >> for s in pepconf: + >> print(s) # apply transformation + """ + + @abstractmethod + def __call__(self, sample: Data) -> Data: + """Apply transformation to `Data` object and return transformed `Data` object.""" + pass + + +class DummyTransform(BaseTransform): + """Dummy transformation for testing purposes.""" + + def __call__(self, sample: Data) -> Data: + sample.uid = sample.uid + "_edit" + return sample diff --git a/src/transform/filter_rmsd.py b/src/transform/filter_rmsd.py new file mode 100644 index 0000000..537dd05 --- /dev/null +++ b/src/transform/filter_rmsd.py @@ -0,0 +1,18 @@ +from tqdm import tqdm +from .base_processor import BaseProcessor + + +class FilterRmsd(BaseProcessor): + """Filter a dataset based on values of the RMSD between + sample geometries on GFN-FF and reference level. + """ + + def __init__(self, threshold: float): + self.thr = threshold # max allowed value for RMSD + + def __call__(self, dataset): + idxs = [] + for i, sample in tqdm(enumerate(dataset), desc="Filtering RMSD"): + if sample.rmsd < self.thr: + idxs.append(i) + return dataset.index_select(idxs) diff --git a/src/transform/graph_construction.py b/src/transform/graph_construction.py new file mode 100644 index 0000000..8b0f38e --- /dev/null +++ b/src/transform/graph_construction.py @@ -0,0 +1,27 @@ +from torch_geometric.nn import radius_graph +from torch_geometric.data import Data +from .base_transform import BaseTransform + + +class RadiusGraph(BaseTransform): + """ + Transformation for setting up the graph topology (specified by 'edge_index' tensor) for a given cutoff radius + """ + + def __init__(self, cutoff: float): + """ + :param cutoff: Cutoff radius that is used to setup the graph topology + """ + super(RadiusGraph, self).__init__() + self.cutoff = cutoff + + def __call__(self, sample: Data) -> Data: + batch = sample["batch"] if "batch" in sample.keys() else None + edge_index = radius_graph( + x=sample["pos"], + r=self.cutoff, + batch=batch, + max_num_neighbors=320, + ).long() + sample = sample.update({"edge_index": edge_index}) + return sample diff --git a/src/transform/pipeline.py b/src/transform/pipeline.py new file mode 100644 index 0000000..dfc91f9 --- /dev/null +++ b/src/transform/pipeline.py @@ -0,0 +1,14 @@ +from torch_geometric.data import Data +from .base_transform import BaseTransform + + +class PipelineTransform(BaseTransform): + """Combination of multiple transformations applied in consecutive order to a given `Data` object.""" + + def __init__(self, tranformations: list[BaseTransform]): + self.tranformations = tranformations + + def __call__(self, sample: Data) -> Data: + for trafo in self.tranformations: + sample = trafo(sample) + return sample diff --git a/src/transform/rename.py b/src/transform/rename.py new file mode 100644 index 0000000..c70987a --- /dev/null +++ b/src/transform/rename.py @@ -0,0 +1,30 @@ +from typing import Dict +from torch_geometric.data import Data +from warnings import warn +from .base_transform import BaseTransform + + +class Rename(BaseTransform): + """ + Data transformation renaming certain keys + """ + + def __init__(self, key_mapping: Dict[str, str]): + """ + :param key_mapping: dictionary defining the mapping of the keys. + If is not found in the sample, there will be no error and is skipped. + """ + super(Rename, self).__init__() + self.key_mapping = key_mapping + + def __call__(self, sample: Data) -> Data: + update_dict = {} + for key, new_key in self.key_mapping.items(): + if key in sample.keys(): + update_dict[new_key] = sample[key] + else: + warn( + f"Tried to rename '{key}' to {new_key} but '{key}' was not found in the sample. Not doing anything." + ) + sample.update(update_dict) + return sample diff --git a/src/transform/scale.py b/src/transform/scale.py new file mode 100644 index 0000000..5a9d724 --- /dev/null +++ b/src/transform/scale.py @@ -0,0 +1,30 @@ +from typing import Dict +from warnings import warn +from torch_geometric.data import Data +from .base_transform import BaseTransform + + +class Scale(BaseTransform): + """ + Data transformation scaling certain attributes in the data, e.g. multiplying the gradients by (-1). + """ + + def __init__(self, scaling: Dict[str, float]): + """ + :param scaling: dictionary defining the scaling for certain attributes. + If a key is not found in the sample, the corresponding key will be skipped. + """ + super(Scale, self).__init__() + self.scaling = scaling + + def __call__(self, sample: Data) -> Data: + update_dict = {} + for key, factor in self.scaling.items(): + if key in sample.keys(): + update_dict[key] = factor * sample[key] + else: + warn( + f"Tried to scale '{key}' but '{key}' was not found in the sample. Not doing anything." + ) + sample.update(update_dict) + return sample diff --git a/src/transform/units_conversion.py b/src/transform/units_conversion.py new file mode 100644 index 0000000..9dc4425 --- /dev/null +++ b/src/transform/units_conversion.py @@ -0,0 +1,65 @@ +"""Script to change the units of a given dataset.""" + +import torch +from torch_geometric.transforms import BaseTransform +from src.util.units import AA2AU, AU2KCAL + + +class ConvertUnitsToSI(BaseTransform): + """Change units to SI, namely positions in [Angstrom], energies in [kcal / mol] and gradients in [kcal / mol / Angstrom].""" + + # NOTE: this is a `pyg` Transform object + # ds = ConfRankDataset(fp, transform=ConvertUnitsToSI()) + + energies: list[str] = [ # Eh -> kcal/mol + "add._restraining", + "angle_energy", + "bond_energy", + "bonded_atm_energy", + "dispersion_energy", + "electrostat_energy", + "etot", + "external_energy", + "hb_energy", + "hbb_e", + "hbl_e", + "repulsion_energy", + "torsion_energy", + "total_energy", + "total_energy_ref", + "xb_e", + "xb_energy", + ] + + distances: list[str] = [] # ["pos", "rmsd"] # Bohr -> Angstrom + + gradients: list[str] = ["grad", "grad_ref"] # Eh / Bohr -> kcal/mol / Angstrom + + def forward(self, data): + # NOTE: this function is lazily called when data objects within dataset are accessed. + for attr, value in data: + if attr in self.energies: + data.__setitem__(attr, value * AU2KCAL) + if attr in self.distances: + data.__setitem__(attr, value / AA2AU) + if attr in self.gradients: + data.__setitem__(attr, value * AU2KCAL * AA2AU) + return data + + def apply_on_dict(self, data: dict) -> dict: + """Apply unit conversion on data in a `dict` format.""" + + def _set(attr, value, factor): + """Handle different dimensionalities, i.e. scalars, lists, tensors.""" + value = torch.tensor(value) + value = value * factor + data[attr] = value.tolist() + + for attr, value in data.items(): + if attr in self.energies: + _set(attr, value, AU2KCAL) + if attr in self.distances: + _set(attr, value, 1 / AA2AU) + if attr in self.gradients: + _set(attr, value, AU2KCAL * AA2AU) + return data diff --git a/src/util/__init__.py b/src/util/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/util/__init__.py @@ -0,0 +1 @@ + diff --git a/src/util/__pycache__/__init__.cpython-311.pyc b/src/util/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6c7eb65c1864b3a93469a195e668411c2884e6e GIT binary patch literal 161 zcmZ3^%ge<81o9%g(inmCV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QG?9cNf23Zn#v!I?OKXc*$GS(5KebX>Fnbj zv3Hasg#wtu1%x~_PGG=9ngVJOq%M+&J{3iun*Iexfq;kugcv9SG;eg1poX71yC;6h zHqeJIm-{ihJF`2x^P9Op#^VtL>C+>B%bkfM^iSGpCN=_{?;k*M4{1o_43zb2F3Ulm zH~74e6*x*03^6ZeMb_qwKsEq0(Fo>4*-&1}N{k*b!ud!x!rDP2nvZ2;tQ|7q`9wCs z+LF5KsiCX-&m>meCFw*)dMeD&5PT|OKbML6# z(t7crmc$_(YtwR))^|IQ9nuZ~-(ihgMCpO==-P!e=ZUuzVnQuxih~_JkJG#txMu2> z=}B)D9HLWi>x{_B^G$iSs~Fxz_#=3P4+c z|Mxj4?x7OehWg2*abLiyFIy`&IgQ(H^*ZO5IE~lPule>dyH~;?g@wmlU7c3jcE-R3~N1)O(#=W_j(D=TWfeJrxV**BGxU^54;obx2K>eqk63G zqst#&uJl)9BemE_`FuUv^U89-{hJm#wcKH+`9nAerZ}g4lr^mJF@#~ZF_=VZ= z`PuPBFGewRNhKKIRP%aP*9(|{0+l>=FtNP|2z2n0rmIdm4!zEctH)TVDrZK=F6s{=Io&y8{-0JNsUu^JF;41fRELDq* zxyOI%=W1-O7MuGfIrvGc68d=b?&vqk)F%TU58fSYgiyGr5kX_=t?-@6sx<0KqmLRq z2R!zqm>d5^Ra&e`i*7SNdi1CvN`c5;VyKoF-!A?69ruOP)x_(y#Ou3>b32K1e}A`{ zn6D+~cN2>{iN$K-S}k#HOV~?{RP1VEtdj{KTCbGa(d*W~%~#VxZc9%%&8@pNr$rhL&& zj8;XtJ~FmDlGz!_R7XzKMoz#iqbfeXCx&ld+FGxqtK#vRc-(Dfn>wo7b7(Grz78bk zhJ>#V&-I*{ME{sfo_RyK3I8Ch z*sO7FR|)7QXdSL|Gf?6_5K7!;(Cl@ng#A=PUme87AdgBxEvTcC_&N6*ZZlL0J>5H$ z0ww;dz(IT|g#ced3I|UrLZ96%vkkfB+h*k};Nj!~cxeaxv+P(hdyQ;H=ZdzvX60s_ zGAXNuV%sv{gKSue#uxxR*afWWIbHQxW#o%FuznL$6aed6(>1J3`nbfF-=cuE48vv) z69VASDVN%#>deZ1J*AM9B83~-$}y2kZTt3D`Byl*e+mXTri$5E+iqzfCQ%(w_nzj+ zH0b|NC@qo~p#gXZXYmqD4@(5wma&G3*F⁢?fecbdrH3i~*UTEtwV!8=^~XoiSvv zsTZP`K$CC(abQnTnoh+AnSlu>;cu6qD5D>TP%?ElQaM%a9k2C{moGj@jk@wnwbaS- zTlM5~mD9E4i{(oXLPOznv0 z>Cw$W>-Z)3zfEqFcY;k0;4wSf9p2jM@VZXRlfwLtw#I!H1dHP#KovRz)J+~duCU#T z3EI-o0l&S5?6FG_GnM$?A}#R0t$~@r-vzq;|F1aMMd&8;@1(~`wk2v$L@+dZYa;|$ zX?Wv4d!`q#0X|I56-|}-TxwmuX*BIiwZKeNXa9}+Hn_abEQ&sA8NcUX(*}RtWl7EV z{uGl}nyjcxWLVH`O!M@0`o_)%(m{_eI9^BtZ)jqO^7D#{0Y=qT=xsymAvjrO1LA#d*p(We88eo!bzf zP#BRPb4ST6NZi=)cg~nXbkx?2oGCaNdCs;0O=O6m0GbpNmI9mrTu^k`|K?f*r5l)G zn6nDA_9L17D*yysRm>GX%Ie;FI_&Y5?THj(Aa+_5x^5o2w}W%>ngoiUMUn{Gc2gEX;{*N5%MZ!MT;x&xBmqNfJY1s4&Pnb zO}(&_dSUy$YU)HSb>f4!%ICJy-zEn44jta>?ceJg+UtJ~aw8t<4>k}K<;#s;1lV%4 za;$RfZX7TtHc&adJ3O&7JW(BsI+b_a^@zgx=>a^Z)j% zhqPMYfAFi;#MYvR0*X)77~2P--aF@ZLPKt7XitiRg!m=?p}^B90*VGJf+#jr3D=^> zT=5tKsx-fFHT@g|Z}J8l-jnD@Ht5Rs139WJa*PfHopq{1LVYfwkmL2hcV&h>KRdGw z-`s@4h1cIfZGC*RyYrGAL;j~YyU8v_^uwH8a@TDCGo4*Ii!D6?oEi)JJQ4N2mOaDSGUujE!Fy`8UZ>QM9Bevo`izYty8tY WKqK5-#@719=|OI+fu1aw;{OZ(QKdEj literal 0 HcmV?d00001 diff --git a/src/util/__pycache__/periodic_table.cpython-311.pyc b/src/util/__pycache__/periodic_table.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5777117c7568f70221e197ec10f08d815ba2e8d GIT binary patch literal 4787 zcma);X;2jD8OOV4xCBAL8(nSi#Dl^61my;S0U10%5ohM<8Ag~H?3qzfJd&7%O-$mh zo0vOnl6Bpf``q03eF-7aOjRmYZ7QiJpQg&n7xN)``gvX`W+hc=;Mf1(`@a2l_j}Cn zYJPsMMo0F!f#x{% zs)Ks+252NUK{N3%v=CdNjp&AUVh0={c7li41xJa;;5hLFbQ62vB=HpV5>G=P@eFv0 zKJXI(Om(LK1j+j$L_7-v#6buXBM>DH!8xJ@F=8AN#Pe{0coBw)m*6sS1g;RT!ZqS` zcnk5Z@HXNNcsubO@J@B7QFxyGUGQ$=O?VISz3@KbEqHo6~O#BJ_l=w6FIq?_pOX9EK*Y=&@ zH}KmKoqII+-3`su#`LKrI`e#&9uAOps4Wb|ahgqcC^B>w|> z7EF4yvOgyECbYXxI+{$Rbx)x+gia1MCfa)oAG4th7P=Yk#!H*j$Ku#7`n6gP6{^5Z5BoeA75p&z$! z=m0F|P#r`Uhg!f24$TqV%E1%CZ4!LAokN$2JM5r0<4!I(Vd5?hwK3eyp~j1QIJA0k zuT+`1k3)xr`#HD)4@gNED>-<=SjC|}fd@G>0v_Vf<-=+Yb;DT0p(%>B9O^BslQvNp6vF92zV@uVH}qj-u7 z+(XzaH3>Y;p*oCx9BRXOhC|H&dZi?QJ`VLU^mAwkBXDQ~GV7~s%qVUU8~-i=d- zz5VRH_{5$F`zvWg>n%P-+w_Wl#WA9fQR%bCT`IaiT|DBr?YOzzqrIk;>B-E&=mpa% zb0!_-6-j*{sh^M8-5ZASwAIxg4Vx}2YFc1gt|2RW*7V0?u3#V%2=w`*kw71K<6c+H zZ-s`ayu92Mv;3|^JT&MUGObV)LjJzE*EeXE4-G%fJwO`wN5ex^)C~C z+0%1Ji`y5fWA=~D5p9gd@7W`L)Ih&Wt?o~^q>oC+rBp5rl+kF$jd;Ilc;nG<$Ztdv zVV`Lkfv9D8jbP}!88Osj8}iuYxqMl*6^!lUWOtJOX2ynC+$!5=bWyjnUfjtqKI8gO zBreU>h*J5q%%7=L+ z6L}?Lul%%r(&pY?y1YB=-TVK{efk{N zX}$BN_mX2o8{pZtGDdX2L)Sdo&H147y#G6G-qVLOZQ|1=W7c_3dl_Q`O{1Fj*)Dx% z)@hZ@wn&Uwr~OSmPS3aeij()w!@hBsEv@qqg z2eEV)`{4qcJQGFK#@r9fmJ%Y2bSmLib_EDP=4N>hti z7KFU;WuwR@ zmdzqtSjy!Z7fXfIY-QOdvYlmz$WE4BBD-1ki0ozAC$gXAfJh}vmB>MsL%LYC&RV0i zR!86X3@h+TSQxxwu!n)?SX9zK7^rF&X(Mw7%i;gJ0B6?NnHPJ&3HSfGPQ#DF!MeCH-i#8~2 z6m3%4EIM;24y&pbscKc)ChAt&F502=h-jx$k7yUEJ&Z?1kEz<@q9>Ggi}olzDN5f( zy!CrUPb=*cJ)_hs>Qm|$1*N8FKxt64Uuj76tkMC|nL{zCs=`tgQ5qE;QhH9*QW_JD zD@}-=S9(G8BB?zx!=jf|?PbvsrB_6+D!nFp$f@R?KZsK`N^3>yl-7$jC~XvNVqKkT z7Co%AMYL6Eo2U$hTNSp?D7;>f>U3(08+H00_1@KouG$G#?YOIc(p66dAza7 jD3z6`SB?2o8lCRRiSrDsD9{V+7Id;((CL4Izp4HUf`>c- literal 0 HcmV?d00001 diff --git a/src/util/__pycache__/units.cpython-311.pyc b/src/util/__pycache__/units.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0dc815126b3d306e1fa8b5067cddaca50f11f30 GIT binary patch literal 469 zcmZ3^%ge<81nrXwQol1XFgylvV1OCQ`0N5?OlL@8h+;@#jAGg zT2!2wpQn(Pm|T)yRIJC9&h7H6G|=XsefpgzE7za9u*g3BRzOki>ItXp($hE-)(X#Q zc1Tyg{j)A>kAOqE$b^K?la^m~PM2BX*(G1z>FlS;d5b+hJ|#anKK>S~qoa{y=q*;) zFc8J!4I~wiuKde^3u}cfsRj$Pf09E)Gtmh$}9ki>*?ti7bWYLmSpDWmjeA)tXEL^ zi^C>2KczG$)vkySXeJ{N7smpL56p~=j31bo7+F3rfCvr-PU!~L8w^$tI3z)|?E`k1 z29_HPK@Yg38`y6!xIEzC1&JFzV3!7oTR&iz1<_^?IC&?C%wU};bCFZ7f$au^(**{n IA|9X(0FnocIsgCw literal 0 HcmV?d00001 diff --git a/src/util/deployment.py b/src/util/deployment.py new file mode 100644 index 0000000..bae5afd --- /dev/null +++ b/src/util/deployment.py @@ -0,0 +1,104 @@ +import torch +import os +from warnings import warn +from datetime import datetime +import subprocess +from typing import Union, Optional +from src.models.base import BaseModel +from src.models.DimeNetPP import DimeNetPP +from src.models.SchNet import SchNet +from src.models.MACE import MACE + + +def remove_first_row(string): + lines = string.split("\n") + return "\n".join(lines[1:]) + + +def save_model(model: BaseModel, file_path: str): + save_dict = {} + try: + commit = ( + subprocess.check_output(["git", "rev-parse", "HEAD"]) + .decode("utf-8") + .strip() + ) + save_dict["git_commit"] = commit + except: + warn("Could not get git commit hash. Will not store the commit hash then.") + + try: + env = subprocess.check_output(["micromamba", "list"]).decode("utf-8").strip() + env = remove_first_row(env) + save_dict["env_info"] = env + except: + warn( + "Could not get information on the virtual environment. " + "Probably because micromamba is not installed." + " Will not store information on the environment then." + ) + + # save time when model was exported + save_dict["time_created"] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + + # Save hyperparameters + save_dict["hyperparameters"] = model.hyperparameters + save_dict["state_dict"] = model.state_dict() + torch.save(save_dict, file_path) + + +def restore_model( + file_path: str, + module_cls: Optional[type[BaseModel]] = None, + strict: bool = True, +) -> Union[DimeNetPP]: + """ + :param file_path: Path pointing to model file + :param module_cls: Optional, class for loading the specific model. + If none is provided, the class will be inferred. + :param strict: strict-argument of load_state_dict + :return: model instance + """ + # Load the saved dictionary + save_dict = torch.load(file_path) + # Extract the hyperparameters and state dictionary + hyperparameters = save_dict["hyperparameters"] + state_dict = save_dict["state_dict"] + + if module_cls is not None: + _module_cls = module_cls + else: + _module_cls = model_resolver(filepath=file_path) + + # Create an instance of the model with the loaded hyperparameters + model = _module_cls(**hyperparameters) + + # Load the state dictionary into the model + model.load_state_dict(state_dict, strict=strict) + return model + + +def model_resolver(filepath): + """ + Helper function for inferring the correct model class by the file extension. + :param filepath: Path pointing to a model checkpoint + :return: Model class + """ + name, extension = os.path.splitext(filepath) + mapping_dict = { + "pt": DimeNetPP, # for backwards compatibility + "dimenet": DimeNetPP, + "mace": MACE, + "schnet": SchNet, + } + extension = extension[1:] + assert extension in mapping_dict.keys(), ( + f"Could not find suitable model for file extension .{extension}. " + f"Make sure that the file extension matches one in {mapping_dict.keys()}!" + ) + if extension == "pt": + print( + f"Model file has extension .pt. Assume it is an instance of DimeNetSingle. " + f"If this is not correct, change the file extension" + ) + return mapping_dict[extension] diff --git a/src/util/periodic_table.py b/src/util/periodic_table.py new file mode 100644 index 0000000..a89ad71 --- /dev/null +++ b/src/util/periodic_table.py @@ -0,0 +1,137 @@ +periodic_table = { + "H": 1, + "He": 2, + "Li": 3, + "Be": 4, + "B": 5, + "C": 6, + "N": 7, + "O": 8, + "F": 9, + "Ne": 10, + "Na": 11, + "Mg": 12, + "Al": 13, + "Si": 14, + "P": 15, + "S": 16, + "Cl": 17, + "Ar": 18, + "K": 19, + "Ca": 20, + "Sc": 21, + "Ti": 22, + "V": 23, + "Cr": 24, + "Mn": 25, + "Fe": 26, + "Co": 27, + "Ni": 28, + "Cu": 29, + "Zn": 30, + "Ga": 31, + "Ge": 32, + "As": 33, + "Se": 34, + "Br": 35, + "Kr": 36, + "Rb": 37, + "Sr": 38, + "Y": 39, + "Zr": 40, + "Nb": 41, + "Mo": 42, + "Tc": 43, + "Ru": 44, + "Rh": 45, + "Pd": 46, + "Ag": 47, + "Cd": 48, + "In": 49, + "Sn": 50, + "Sb": 51, + "Te": 52, + "I": 53, + "Xe": 54, + "Cs": 55, + "Ba": 56, + "La": 57, + "Ce": 58, + "Pr": 59, + "Nd": 60, + "Pm": 61, + "Sm": 62, + "Eu": 63, + "Gd": 64, + "Tb": 65, + "Dy": 66, + "Ho": 67, + "Er": 68, + "Tm": 69, + "Yb": 70, + "Lu": 71, + "Hf": 72, + "Ta": 73, + "W": 74, + "Re": 75, + "Os": 76, + "Ir": 77, + "Pt": 78, + "Au": 79, + "Hg": 80, + "Tl": 81, + "Pb": 82, + "Bi": 83, + "Po": 84, + "At": 85, + "Rn": 86, + "Fr": 87, + "Ra": 88, + "Ac": 89, + "Th": 90, + "Pa": 91, + "U": 92, + "Np": 93, + "Pu": 94, + "Am": 95, + "Cm": 96, + "Bk": 97, + "Cf": 98, + "Es": 99, + "Fm": 100, + "Md": 101, + "No": 102, + "Lr": 103, +} +periodic_table_low = {k.lower(): v for k, v in periodic_table.items()} + + +def get_atomic_number(element_symbol: str) -> int: + """ + Get the atomic number for a given element symbol. + + Args: + element_symbol (str): The element symbol. + + Returns: + int: The atomic number corresponding to the element symbol. + Returns -1 if not found. + """ + return periodic_table_low.get(element_symbol.lower(), -1) + + +def get_element_symbol(atomic_number: int) -> str: + """ + Get the element symbol for a given atomic number. + + Args: + atomic_number (int): The atomic number. + + Returns: + str: The element symbol corresponding to the atomic number. + Returns an empty string if not found. + """ + for symbol, number in periodic_table.items(): + if number == atomic_number: + return symbol + return "" diff --git a/src/util/units.py b/src/util/units.py new file mode 100644 index 0000000..b805d0f --- /dev/null +++ b/src/util/units.py @@ -0,0 +1,27 @@ +""" +Unit conversion factors. +""" + +AA2AU = 1.8897261246204404 +"""Factor for conversion from angstrom to atomic units.""" + +EV2AU = 1.0 / 27.21138505 +"""Factor for conversion from eletronvolt to atomic units.""" + +K2AU = 3.166808578545117e-06 +"""Factor for conversion from Kelvin to atomic units for electronic temperatur.""" + +AU2KCAL = 627.5096080305927 +"""Factor for conversion from atomic units (Hartree) to kcal/mol.""" + +CAL2J = 4.184 +"""Factor for conversion from Calorie to Joule""" + +C2AU = 1.0 / 1.60217653e-19 +"""Factor for conversion from Coulomb to to atomic units""" + +J2AU = 1.0 / 4.3597441775e-18 +"""Factor for conversion from Joule to atomic units""" + +VAA2AU = J2AU / (C2AU * AA2AU) +"""Factor for conversion from V/Å = J/(C·Å) to atomic units"""