pax_global_header00006660000000000000000000000064133241711240014510gustar00rootroot0000000000000052 comment=4878fc892f2cbc5cd9f29f7a367d7b05bdeb6ee9 andrewpeterson-amp-4878fc892f2c/000077500000000000000000000000001332417112400164465ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/.gitignore000066400000000000000000000000441332417112400204340ustar00rootroot00000000000000*.pyc *.so *.mod *.o *.py~ *.py.swp andrewpeterson-amp-4878fc892f2c/LICENSE000066400000000000000000001045151332417112400174610ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . andrewpeterson-amp-4878fc892f2c/README000066400000000000000000000040051332417112400173250ustar00rootroot00000000000000# Amp: Atomistic Machine-learning Package # *Amp* is an open-source package designed to easily bring machine-learning to atomistic calculations. This project is being developed at Brown University in the School of Engineering, primarily by Andrew Peterson and Alireza Khorshidi, and is released under the GNU General Public License. *Amp* allows for the modular representation of the potential energy surface, enabling the user to specify or create descriptor and regression methods. This project lives at: https://bitbucket.org/andrewpeterson/amp Documentation lives at: http://amp.readthedocs.org Users' mailing list lives at: https://listserv.brown.edu/?A0=AMP-USERS If you would like to compile a local version of the documentation, see the README file in the docs directory. (This project was formerly known as "Neural". The last stable version of Neural can be found at https://bitbucket.org/andrewpeterson/neural) License ======= This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Installation ============ You can find the installation instructions for this version of Amp in the documentation file `docs/installation.rst`. Documentation ============= We currently host multiple versions of the documentation, which includes installation instructions, at http://amp.readthedocs.io. You can build a local copy of the documentation for this version of Amp. You will find instructions to do this in the "Documentation" section of the file `docs/develop.rst`. andrewpeterson-amp-4878fc892f2c/amp/000077500000000000000000000000001332417112400172235ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/Makefile000066400000000000000000000011351332417112400206630ustar00rootroot00000000000000python2: +$(MAKE) -C model +$(MAKE) -C descriptor f2py -c -m fmodules model.f90 descriptor/cutoffs.f90 descriptor/gaussian.f90 descriptor/zernike.f90 model/neuralnetwork.f90 python3: +$(MAKE) -C model +$(MAKE) -C descriptor f2py3 -c -m fmodules model.f90 descriptor/cutoffs.f90 descriptor/gaussian.f90 descriptor/zernike.f90 model/neuralnetwork.f90 AMPDIR:=$(shell pwd) py2tests: rm -fr /tmp/py2_amptests mkdir -p /tmp/py2_amptests cd /tmp/py2_amptests && nosetests $(AMPDIR)/.. py3tests: rm -fr /tmp/py3_amptests mkdir -p /tmp/py3_amptests cd /tmp/py3_amptests && nosetests3 $(AMPDIR)/.. andrewpeterson-amp-4878fc892f2c/amp/VERSION000066400000000000000000000000061332417112400202670ustar00rootroot000000000000000.6.1 andrewpeterson-amp-4878fc892f2c/amp/__init__.py000066400000000000000000000432471332417112400213460ustar00rootroot00000000000000import os import sys import shutil import numpy as np import tempfile import platform from getpass import getuser from socket import gethostname import subprocess import warnings import ase from ase.calculators.calculator import Calculator, Parameters try: from ase import __version__ as aseversion except ImportError: # We're on ASE 3.9 or older from ase.version import version as aseversion from .utilities import (make_filename, hash_images, Logger, string2dict, logo, now, assign_cores, TrainingConvergenceError, check_images) try: from amp import fmodules except ImportError: warnings.warn('Did not find fortran modules.') else: fmodules_version = 9 wrong_version = fmodules.check_version(version=fmodules_version) if wrong_version: raise RuntimeError('fortran modules are not updated. Recompile ' 'with f2py as described in the README. ' 'Correct version is %i.' % fmodules_version) version_file = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'VERSION') _ampversion = open(version_file).read().strip() #version_file = open(os.path.join(os.path.abspath(__file__), 'VERSION')) #_ampversion = version_file.read().strip() #_ampversion = 'nothing' class Amp(Calculator, object): """Atomistic Machine-Learning Potential (Amp) ASE calculator Parameters ---------- descriptor : object Class representing local atomic environment. model : object Class representing the regression model. Can be only NeuralNetwork for now. Input arguments for NeuralNetwork are hiddenlayers, activation, weights, and scalings; for more information see docstring for the class NeuralNetwork. label : str Default prefix/location used for all files. dblabel : str Optional separate prefix/location for database files, including fingerprints, fingerprint derivatives, and neighborlists. This file location can be shared between calculator instances to avoid re-calculating redundant information. If not supplied, just uses the value from label. cores : int Can specify cores to use for parallel training; if None, will determine from environment envcommand : string For parallel processing across nodes, a command can be supplied here to load the appropriate environment before starting workers. logging : boolean Option to turn off logging; e.g., to speed up force calls. atoms : object ASE atoms objects with positions, symbols, energy, and forces in ASE format. """ implemented_properties = ['energy', 'forces'] def __init__(self, descriptor, model, label='amp', dblabel=None, cores=None, envcommand=None, logging=True, atoms=None): self.logging = logging Calculator.__init__(self, label=label, atoms=atoms) # Note self._log is set and self._printheader is called by above # call when it runs self.set_label. self._parallel = {'envcommand': envcommand} # Note the following are properties: these are setter functions. self.descriptor = descriptor self.model = model self.cores = cores # Note this calls 'assign_cores'. self.dblabel = label if dblabel is None else dblabel @property def cores(self): """ Get or set the cores for the parallel environment. Parameters ---------- cores : int or dictionary Parallel configuration. If cores is an integer, parallelizes over this many processes on machine localhost. cores can also be a dictionary of the type {'node324': 16, 'node325': 16}. If not specified, tries to determine from environment, using amp.utilities.assign_cores. """ return self._parallel['cores'] @cores.setter def cores(self, cores): self._parallel['cores'] = assign_cores(cores, log=self._log) @property def descriptor(self): """ Get or set the atomic descriptor. Parameters ---------- descriptor : object Class instance representing the local atomic environment. """ return self._descriptor @descriptor.setter def descriptor(self, descriptor): descriptor.parent = self # gives the descriptor object a reference to # the main Amp instance. Then descriptor can pull parameters directly # from Amp without needing them to be passed in each method call. self._descriptor = descriptor self.reset() # Clears any old calculations. @property def model(self): """ Get or set the machine-learning model. Parameters ---------- model : object Class instance representing the regression model. """ return self._model @model.setter def model(self, model): model.parent = self # gives the model object a reference to the main # Amp instance. Then model can pull parameters directly from Amp # without needing them to be passed in each method call. self._model = model self.reset() # Clears any old calculations. @classmethod def load(Cls, file, Descriptor=None, Model=None, **kwargs): """Attempts to load calculators and return a new instance of Amp. Only a filename or file-like object is required, in typical cases. If using a home-rolled descriptor or model, also supply uninstantiated classes to those models, as in Model=MyModel. (Not as Model=MyModel()!) Any additional keyword arguments (such as label or dblabel) can be fed through to Amp. Parameters ---------- file : str Name of the file to load data from. Descriptor : object Class representing local atomic environment. Model : object Class representing the regression model. """ if hasattr(file, 'read'): text = file.read() else: with open(file) as f: text = f.read() # Unpack parameter dictionaries. p = string2dict(text) for key in ['descriptor', 'model']: p[key] = string2dict(p[key]) # If modules are not specified, find them. if Descriptor is None: Descriptor = importhelper(p['descriptor'].pop('importname')) if Model is None: Model = importhelper(p['model'].pop('importname')) # Key 'importname' and the value removed so that it is not splatted # into the keyword arguments used to instantiate in the next line. # Instantiate the descriptor and model. descriptor = Descriptor(**p['descriptor']) # ** sends all the key-value pairs at once. model = Model(**p['model']) # Instantiate Amp. calc = Cls(descriptor=descriptor, model=model, **kwargs) calc._log('Loaded file: %s' % file) return calc def set(self, **kwargs): """Function to set parameters. For now, this doesn't do anything as all parameters are within the model and descriptor. """ changed_parameters = Calculator.set(self, **kwargs) if len(changed_parameters) > 0: self.reset() def set_label(self, label): """Sets label, ensuring that any needed directories are made. Parameters ---------- label : str Default prefix/location used for all files. """ Calculator.set_label(self, label) # Create directories for output structure if needed. # Note ASE doesn't do this for us. if self.label: if (self.directory != os.curdir and not os.path.isdir(self.directory)): os.makedirs(self.directory) if self.logging is True: self._log = Logger(make_filename(self.label, '-log.txt')) else: self._log = Logger(None) self._printheader(self._log) def calculate(self, atoms, properties, system_changes): """Calculation of the energy of system and forces of all atoms. """ # The inherited method below just sets the atoms object, # if specified, to self.atoms. Calculator.calculate(self, atoms, properties, system_changes) log = self._log log('Calculation requested.') images = hash_images([self.atoms]) key = list(images.keys())[0] if properties == ['energy']: log('Calculating potential energy...', tic='pot-energy') self.descriptor.calculate_fingerprints(images=images, log=log, calculate_derivatives=False) energy = self.model.calculate_energy( self.descriptor.fingerprints[key]) self.results['energy'] = energy log('...potential energy calculated.', toc='pot-energy') if properties == ['forces']: log('Calculating forces...', tic='forces') self.descriptor.calculate_fingerprints(images=images, log=log, calculate_derivatives=True) forces = \ self.model.calculate_forces( self.descriptor.fingerprints[key], self.descriptor.fingerprintprimes[key]) self.results['forces'] = forces log('...forces calculated.', toc='forces') def train(self, images, overwrite=False, ): """Fits the model to the training images. Parameters ---------- images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. overwrite : bool If an output file with the same name exists, overwrite it. """ log = self._log log('\nAmp training started. ' + now() + '\n') log('Descriptor: %s\n (%s)' % (self.descriptor.__class__.__name__, self.descriptor)) log('Model: %s\n (%s)' % (self.model.__class__.__name__, self.model)) images = hash_images(images, log=log) log('\nDescriptor\n==========') train_forces = self.model.forcetraining # True / False check_images(images, forces=train_forces) self.descriptor.calculate_fingerprints( images=images, parallel=self._parallel, log=log, calculate_derivatives=train_forces) log('\nModel fitting\n=============') result = self.model.fit(trainingimages=images, descriptor=self.descriptor, log=log, parallel=self._parallel) if result is True: log('Amp successfully trained. Saving current parameters.') filename = self.label + '.amp' else: log('Amp not trained successfully. Saving current parameters.') filename = make_filename(self.label, '-untrained-parameters.amp') filename = self.save(filename, overwrite) log('Parameters saved in file "%s".' % filename) log("This file can be opened with `calc = Amp.load('%s')`" % filename) if result is False: raise TrainingConvergenceError('Amp did not converge upon ' 'training. See log file for' ' more information.') def save(self, filename, overwrite=False): """Saves the calculator in a way that it can be re-opened with load. Parameters ---------- filename : str File object or path to the file to write to. overwrite : bool If an output file with the same name exists, overwrite it. """ if os.path.exists(filename): if overwrite is False: oldfilename = filename filename = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.amp').name self._log('File "%s" exists. Instead saving to "%s".' % (oldfilename, filename)) else: oldfilename = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.amp').name self._log('Overwriting file: "%s". Moving original to "%s".' % (filename, oldfilename)) shutil.move(filename, oldfilename) descriptor = self.descriptor.tostring() model = self.model.tostring() p = Parameters({'descriptor': descriptor, 'model': model}) p.write(filename) return filename def _printheader(self, log): """Prints header to log file; inspired by that in GPAW. """ log(logo) log('Amp: Atomistic Machine-learning Package') log('Developed by Andrew Peterson, Alireza Khorshidi, and others,') log('Brown University.') log('PI Website: http://brown.edu/go/catalyst') log('Official repository: http://bitbucket.org/andrewpeterson/amp') log('Official documentation: http://amp.readthedocs.org/') log('Citation:') log(' Alireza Khorshidi & Andrew A. Peterson,') log(' Computer Physics Communications 207: 310-324 (2016).') log(' http://doi.org/10.1016/j.cpc.2016.05.010') log('=' * 70) log('User: %s' % getuser()) log('Hostname: %s' % gethostname()) log('Date: %s' % now(with_utc=True)) uname = platform.uname() log('Architecture: %s' % uname[4]) log('PID: %s' % os.getpid()) log('Amp version: %s' % _ampversion) ampdirectory = os.path.dirname(os.path.abspath(__file__)) log('Amp directory: %s' % ampdirectory) commithash, commitdate = get_git_commit(ampdirectory) log(' Last commit: %s' % commithash) log(' Last commit date: %s' % commitdate) log('Python: v{0}.{1}.{2}: %s'.format(*sys.version_info[:3]) % sys.executable) log('ASE v%s: %s' % (aseversion, os.path.dirname(ase.__file__))) log('NumPy v%s: %s' % (np.version.version, os.path.dirname(np.__file__))) # SciPy is not a strict dependency. try: import scipy log('SciPy v%s: %s' % (scipy.version.version, os.path.dirname(scipy.__file__))) except ImportError: log('SciPy: not available') # ZMQ an pxssh are only necessary for parallel calculations. try: import zmq log('ZMQ/PyZMQ v%s/v%s: %s' % (zmq.zmq_version(), zmq.pyzmq_version(), os.path.dirname(zmq.__file__))) except ImportError: log('ZMQ: not available') try: import pxssh log('pxssh: %s' % os.path.dirname(pxssh.__file__)) except ImportError: log('pxssh: Not available from pxssh.') try: from pexpect import pxssh except ImportError: log('pxssh: Not available from pexpect.') else: import pexpect log('pxssh (via pexpect v%s): %s' % (pexpect.__version__, pxssh.__file__)) log('=' * 70) def importhelper(importname): """Manually compiled list of available modules. This is to prevent the execution of arbitrary (potentially malicious) code. However, since there is an `eval` statement in string2dict maybe this is silly. """ if importname == '.descriptor.gaussian.Gaussian': from .descriptor.gaussian import Gaussian as Module elif importname == '.descriptor.zernike.Zernike': from .descriptor.zernike import Zernike as Module elif importname == '.descriptor.bispectrum.Bispectrum': from .descriptor.bispectrum import Bispectrum as Module elif importname == '.model.neuralnetwork.NeuralNetwork': from .model.neuralnetwork import NeuralNetwork as Module elif importname == '.model.neuralnetwork.tflow': from .model.tflow import NeuralNetwork as Module elif importname == '.model.LossFunction': from .model import LossFunction as Module else: raise NotImplementedError( 'Attempt to import the module %s. Was this intended? ' 'If so, trying manually importing this module and ' 'feeding it to Amp.load. To avoid this error, this ' 'module can be added to amp.importhelper.' % importname) return Module def get_git_commit(ampdirectory): """Attempts to get the last git commit from the amp directory. """ pwd = os.getcwd() os.chdir(ampdirectory) try: with open(os.devnull, 'w') as devnull: output = subprocess.check_output(['git', 'log', '-1', '--pretty=%H\t%ci'], stderr=devnull) except: output = b'unknown hash\tunknown date' output = output.strip() commithash, commitdate = output.split(b'\t') os.chdir(pwd) return commithash, commitdate andrewpeterson-amp-4878fc892f2c/amp/analysis.py000066400000000000000000000644351332417112400214340ustar00rootroot00000000000000#!/usr/bin/env python from . import Amp from .utilities import now, hash_images, make_filename import os import numpy as np from matplotlib import pyplot from matplotlib.backends.backend_pdf import PdfPages from matplotlib import rcParams rcParams.update({'figure.autolayout': True}) def plot_sensitivity(load, images, d=0.0001, label='sensitivity', dblabel=None, plotfile=None, overwrite=False, energy_coefficient=1.0, force_coefficient=0.04): """Returns the plot of loss function in terms of perturbed parameters. Takes the load file and images. Any other keyword taken by the Amp calculator can be fed to this class also. Parameters ---------- load : str Path for loading an existing ".amp" file. Should be fed like 'load="filename.amp"'. images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. d : float The amount of perturbation in each parameter. label : str Default prefix/location used for all files. dblabel : str Optional separate prefix/location of database files, including fingerprints, fingerprint primes, and neighborlists, to avoid calculating them. If not supplied, just uses the value from label. plotfile : Object File for the plot. overwrite : bool If a plot or an script containing values found overwrite it. energy_coefficient : float Coefficient of energy loss in the total loss function. force_coefficient : float Coefficient of force loss in the total loss function. """ from amp.model import LossFunction calc = Amp.load(file=load) if plotfile is None: plotfile = make_filename(label, '-plot.pdf') if (not overwrite) and os.path.exists(plotfile): raise IOError('File exists: %s.\nIf you want to overwrite,' ' set overwrite=True or manually delete.' % plotfile) calc.dblabel = label if dblabel is None else dblabel if force_coefficient == 0.: calculate_derivatives = False else: calculate_derivatives = True calc._log('\nAmp sensitivity analysis started. ' + now() + '\n') calc._log('Descriptor: %s' % calc.descriptor.__class__.__name__) calc._log('Model: %s' % calc.model.__class__.__name__) images = hash_images(images) calc._log('\nDescriptor\n==========') calc.descriptor.calculate_fingerprints( images=images, parallel=calc._parallel, log=calc._log, calculate_derivatives=calculate_derivatives) vector = calc.model.vector.copy() lossfunction = LossFunction(energy_coefficient=energy_coefficient, force_coefficient=force_coefficient, parallel=calc._parallel, ) calc.model.lossfunction = lossfunction # Set up local loss function. calc.model.lossfunction.attach_model( calc.model, fingerprints=calc.descriptor.fingerprints, fingerprintprimes=calc.descriptor.fingerprintprimes, images=images) originalloss = calc.model.lossfunction.get_loss( vector, lossprime=False)['loss'] calc._log('\n Perturbing parameters...', tic='perturb') allparameters = [] alllosses = [] num_parameters = len(vector) for count in range(num_parameters): calc._log('parameter %i out of %i' % (count + 1, num_parameters)) parameters = [] losses = [] # parameter is perturbed -d and loss function calculated. vector[count] -= d parameters.append(vector[count]) perturbedloss = calc.model.lossfunction.get_loss( vector, lossprime=False)['loss'] losses.append(perturbedloss) vector[count] += d parameters.append(vector[count]) losses.append(originalloss) # parameter is perturbed +d and loss function calculated. vector[count] += d parameters.append(vector[count]) perturbedloss = calc.model.lossfunction.get_loss( vector, lossprime=False)['loss'] losses.append(perturbedloss) allparameters.append(parameters) alllosses.append(losses) # returning back to the original value. vector[count] -= d calc._log('...parameters perturbed and loss functions calculated', toc='perturb') calc._log('Plotting loss function vs perturbed parameters...', tic='plot') with PdfPages(plotfile) as pdf: count = 0 for parameter in vector: fig = pyplot.figure() ax = fig.add_subplot(111) ax.plot(allparameters[count], alllosses[count], marker='o', linestyle='--', color='b',) xmin = allparameters[count][0] - \ 0.1 * (allparameters[count][-1] - allparameters[count][0]) xmax = allparameters[count][-1] + \ 0.1 * (allparameters[count][-1] - allparameters[count][0]) ymin = min(alllosses[count]) - \ 0.1 * (max(alllosses[count]) - min(alllosses[count])) ymax = max(alllosses[count]) + \ 0.1 * (max(alllosses[count]) - min(alllosses[count])) ax.set_xlim([xmin, xmax]) ax.set_ylim([ymin, ymax]) ax.set_xlabel('parameter no %i' % count) ax.set_ylabel('loss function') pdf.savefig(fig) pyplot.close(fig) count += 1 calc._log(' ...loss functions plotted.', toc='plot') def plot_parity(load, images, label='parity', dblabel=None, plot_forces=True, plotfile=None, color='b.', overwrite=False, returndata=False, energy_coefficient=1.0, force_coefficient=0.04): """Makes a parity plot of Amp energies and forces versus real energies and forces. Parameters ---------- load : str Path for loading an existing ".amp" file. Should be fed like 'load="filename.amp"'. images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. label : str Default prefix/location used for all files. dblabel : str Optional separate prefix/location of database files, including fingerprints, fingerprint primes, and neighborlists, to avoid calculating them. If not supplied, just uses the value from label. plot_forces : bool Determines whether or not forces should be plotted as well. plotfile : Object File for the plot. color : str Plot color. overwrite : bool If a plot or an script containing values found overwrite it. returndata : bool Whether to return a reference to the figures and their data or not. energy_coefficient : float Coefficient of energy loss in the total loss function. force_coefficient : float Coefficient of force loss in the total loss function. """ calc = Amp.load(file=load, label=label, dblabel=dblabel) if plotfile is None: plotfile = make_filename(label, '-plot.pdf') if (not overwrite) and os.path.exists(plotfile): raise IOError('File exists: %s.\nIf you want to overwrite,' ' set overwrite=True or manually delete.' % plotfile) if (force_coefficient != 0.) or (plot_forces is True): calculate_derivatives = True else: calculate_derivatives = False calc._log('\nAmp parity plot started. ' + now() + '\n') calc._log('Descriptor: %s' % calc.descriptor.__class__.__name__) calc._log('Model: %s' % calc.model.__class__.__name__) images = hash_images(images, log=calc._log) calc._log('\nDescriptor\n==========') calc.descriptor.calculate_fingerprints( images=images, parallel=calc._parallel, log=calc._log, calculate_derivatives=calculate_derivatives) calc._log('Calculating potential energies...', tic='pot-energy') energy_data = {} for hash, image in images.iteritems(): amp_energy = calc.model.calculate_energy( calc.descriptor.fingerprints[hash]) actual_energy = image.get_potential_energy(apply_constraint=False) energy_data[hash] = [actual_energy, amp_energy] calc._log('...potential energies calculated.', toc='pot-energy') min_act_energy = min([energy_data[hash][0] for hash, image in images.iteritems()]) max_act_energy = max([energy_data[hash][0] for hash, image in images.iteritems()]) if plot_forces is False: fig = pyplot.figure(figsize=(5., 5.)) ax = fig.add_subplot(111) else: fig = pyplot.figure(figsize=(5., 10.)) ax = fig.add_subplot(211) calc._log('Plotting energies...', tic='energy-plot') for hash, image in images.iteritems(): ax.plot(energy_data[hash][0], energy_data[hash][1], color) # draw line ax.plot([min_act_energy, max_act_energy], [min_act_energy, max_act_energy], 'r-', lw=0.3,) ax.set_xlabel("ab initio energy, eV") ax.set_ylabel("Amp energy, eV") ax.set_title("Energies") calc._log('...energies plotted.', toc='energy-plot') if plot_forces is True: ax = fig.add_subplot(212) calc._log('Calculating forces...', tic='forces') force_data = {} for hash, image in images.iteritems(): amp_forces = \ calc.model.calculate_forces( calc.descriptor.fingerprints[hash], calc.descriptor.fingerprintprimes[hash]) actual_forces = image.get_forces(apply_constraint=False) force_data[hash] = [actual_forces, amp_forces] calc._log('...forces calculated.', toc='forces') min_act_force = min([force_data[hash][0][index][k] for hash, image in images.iteritems() for index in range(len(image)) for k in range(3)]) max_act_force = max([force_data[hash][0][index][k] for hash, image in images.iteritems() for index in range(len(image)) for k in range(3)]) calc._log('Plotting forces...', tic='force-plot') for hash, image in images.iteritems(): for index in range(len(image)): for k in range(3): ax.plot(force_data[hash][0][index][k], force_data[hash][1][index][k], color) # draw line ax.plot([min_act_force, max_act_force], [min_act_force, max_act_force], 'r-', lw=0.3,) ax.set_xlabel("ab initio force, eV/Ang") ax.set_ylabel("Amp force, eV/Ang") ax.set_title("Forces") calc._log('...forces plotted.', toc='force-plot') fig.savefig(plotfile) if returndata: if plot_forces is False: return fig, energy_data else: return fig, energy_data, force_data def plot_error(load, images, label='error', dblabel=None, plot_forces=True, plotfile=None, color='b.', overwrite=False, returndata=False, energy_coefficient=1.0, force_coefficient=0.04): """Makes an error plot of Amp energies and forces versus real energies and forces. Parameters ---------- load : str Path for loading an existing ".amp" file. Should be fed like 'load="filename.amp"'. images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. label : str Default prefix/location used for all files. dblabel : str Optional separate prefix/location of database files, including fingerprints, fingerprint primes, and neighborlists, to avoid calculating them. If not supplied, just uses the value from label. plot_forces : bool Determines whether or not forces should be plotted as well. plotfile : Object File for the plot. color : str Plot color. overwrite : bool If a plot or an script containing values found overwrite it. returndata : bool Whether to return a reference to the figures and their data or not. energy_coefficient : float Coefficient of energy loss in the total loss function. force_coefficient : float Coefficient of force loss in the total loss function. """ calc = Amp.load(file=load) if plotfile is None: plotfile = make_filename(label, '-plot.pdf') if (not overwrite) and os.path.exists(plotfile): raise IOError('File exists: %s.\nIf you want to overwrite,' ' set overwrite=True or manually delete.' % plotfile) calc.dblabel = label if dblabel is None else dblabel if (force_coefficient != 0.) or (plot_forces is True): calculate_derivatives = True else: calculate_derivatives = False calc._log('\nAmp error plot started. ' + now() + '\n') calc._log('Descriptor: %s' % calc.descriptor.__class__.__name__) calc._log('Model: %s' % calc.model.__class__.__name__) images = hash_images(images, log=calc._log) calc._log('\nDescriptor\n==========') calc.descriptor.calculate_fingerprints( images=images, parallel=calc._parallel, log=calc._log, calculate_derivatives=calculate_derivatives) calc._log('Calculating potential energy errors...', tic='pot-energy') energy_data = {} for hash, image in images.iteritems(): no_of_atoms = len(image) amp_energy = calc.model.calculate_energy( calc.descriptor.fingerprints[hash]) actual_energy = image.get_potential_energy(apply_constraint=False) act_energy_per_atom = actual_energy / no_of_atoms energy_error = abs(amp_energy - actual_energy) / no_of_atoms energy_data[hash] = [act_energy_per_atom, energy_error] calc._log('...potential energy errors calculated.', toc='pot-energy') # calculating energy per atom rmse energy_square_error = 0. for hash, image in images.iteritems(): energy_square_error += energy_data[hash][1] ** 2. energy_per_atom_rmse = np.sqrt(energy_square_error / len(images)) min_act_energy_per_atom = min([energy_data[hash][0] for hash, image in images.iteritems()]) max_act_energy_per_atom = max([energy_data[hash][0] for hash, image in images.iteritems()]) if plot_forces is False: fig = pyplot.figure(figsize=(5., 5.)) ax = fig.add_subplot(111) else: fig = pyplot.figure(figsize=(5., 10.)) ax = fig.add_subplot(211) calc._log('Plotting energy errors...', tic='energy-plot') for hash, image in images.iteritems(): ax.plot(energy_data[hash][0], energy_data[hash][1], color) # draw horizontal line for rmse ax.plot([min_act_energy_per_atom, max_act_energy_per_atom], [energy_per_atom_rmse, energy_per_atom_rmse], color='black', linestyle='dashed', lw=1,) ax.text(max_act_energy_per_atom, energy_per_atom_rmse, 'energy rmse = %6.5f' % energy_per_atom_rmse, ha='right', va='bottom', color='black') ax.set_xlabel("ab initio energy (eV) per atom") ax.set_ylabel("$|$ab initio energy - Amp energy$|$ / number of atoms") ax.set_title("Energies") calc._log('...energy errors plotted.', toc='energy-plot') if plot_forces is True: ax = fig.add_subplot(212) calc._log('Calculating force errors...', tic='forces') force_data = {} for hash, image in images.iteritems(): amp_forces = \ calc.model.calculate_forces( calc.descriptor.fingerprints[hash], calc.descriptor.fingerprintprimes[hash]) actual_forces = image.get_forces(apply_constraint=False) force_data[hash] = [ actual_forces, abs(np.array(amp_forces) - np.array(actual_forces))] calc._log('...force errors calculated.', toc='forces') # calculating force rmse force_square_error = 0. for hash, image in images.iteritems(): no_of_atoms = len(image) for index in range(no_of_atoms): for k in range(3): force_square_error += \ ((1.0 / 3.0) * force_data[hash][1][index][k] ** 2.) / \ no_of_atoms force_rmse = np.sqrt(force_square_error / len(images)) min_act_force = min([force_data[hash][0][index][k] for hash, image in images.iteritems() for index in range(len(image)) for k in range(3)]) max_act_force = max([force_data[hash][0][index][k] for hash, image in images.iteritems() for index in range(len(image)) for k in range(3)]) calc._log('Plotting force errors...', tic='force-plot') for hash, image in images.iteritems(): for index in range(len(image)): for k in range(3): ax.plot(force_data[hash][0][index][k], force_data[hash][1][index][k], color) # draw horizontal line for rmse ax.plot([min_act_force, max_act_force], [force_rmse, force_rmse], color='black', linestyle='dashed', lw=1,) ax.text(max_act_force, force_rmse, 'force rmse = %5.4f' % force_rmse, ha='right', va='bottom', color='black',) ax.set_xlabel("ab initio force, eV/Ang") ax.set_ylabel("$|$ab initio force - Amp force$|$") ax.set_title("Forces") calc._log('...force errors plotted.', toc='force-plot') fig.savefig(plotfile) if returndata: if plot_forces is False: return fig, energy_data else: return fig, energy_data, force_data def read_trainlog(logfile, verbose=True): """Reads the log file from the training process, returning the relevant parameters. Parameters ---------- logfile : str Name or path to the log file. verbose : bool Write out logfile during analysis. """ data = {} with open(logfile, 'r') as f: lines = f.read().splitlines() def print_(text): if verbose: print(text) # Get number of images. for line in lines: if 'unique images after hashing.' in line: no_images = int(line.split()[0]) break data['no_images'] = no_images # Find where convergence data starts. startline = None for index, line in enumerate(lines): if 'Loss function convergence criteria:' in line: startline = index data['convergence'] = {} d = data['convergence'] break else: return data # Get convergence parameters. ready = [False] * 7 for index, line in enumerate(lines[startline:]): if 'energy_rmse:' in line: ready[0] = True d['energy_rmse'] = float(line.split(':')[-1]) elif 'force_rmse:' in line: ready[1] = True _ = line.split(':')[-1].strip() if _ == 'None': d['force_rmse'] = None trainforces = False else: d['force_rmse'] = float(line.split(':')[-1]) trainforces = True print_('train forces: %s' % trainforces) elif 'force_coefficient:' in line: ready[2] = True _ = line.split(':')[-1].strip() if _ == 'None': d['force_coefficient'] = 0. else: d['force_coefficient'] = float(_) elif 'energy_coefficient:' in line: ready[3] = True d['energy_coefficient'] = float(line.split(':')[-1]) elif 'energy_maxresid:' in line: ready[5] = True _ = line.split(':')[-1].strip() if _ == 'None': d['energy_maxresid'] = None else: d['energy_maxresid'] = float(_) elif 'force_maxresid:' in line: ready[6] = True _ = line.split(':')[-1].strip() if _ == 'None': d['force_maxresid'] = None else: d['force_maxresid'] = float(_) elif 'Step' in line and 'Time' in line: ready[4] = True startline += index + 2 if ready == [True] * 7: break for _ in d.iteritems(): print_('{}: {}'.format(_[0], _[1])) E = d['energy_rmse']**2 * no_images if trainforces: F = d['force_rmse']**2 * no_images else: F = 0. costfxngoal = d['energy_coefficient'] * E + d['force_coefficient'] * F d['costfxngoal'] = costfxngoal # Extract data (emrs and fmrs are max residuals). steps, es, fs, emrs, fmrs, costfxns = [], [], [], [], [], [] costfxnEs, costfxnFs = [], [] index = startline d['converged'] = None while index < len(lines): line = lines[index] if 'Saving checkpoint data.' in line: index += 1 continue elif 'Overwriting file' in line: index += 1 continue elif 'optimization completed successfully.' in line: # old version d['converged'] = True break elif '...optimization successful.' in line: d['converged'] = True break elif 'could not find parameters for the' in line: break elif '...optimization unsuccessful.' in line: d['converged'] = False break print_(line) if trainforces: step, time, costfxn, e, _, emr, _, f, _, fmr, _ = line.split() fs.append(float(f)) fmrs.append(float(fmr)) F = float(f)**2 * no_images costfxnFs.append(d['force_coefficient'] * F / float(costfxn)) else: step, time, costfxn, e, _, emr, _ = line.split() steps.append(int(step)) es.append(float(e)) emrs.append(float(emr)) costfxns.append(costfxn) E = float(e)**2 * no_images costfxnEs.append(d['energy_coefficient'] * E / float(costfxn)) index += 1 d['steps'] = steps d['es'] = es d['fs'] = fs d['emrs'] = emrs d['fmrs'] = fmrs d['costfxns'] = costfxns d['costfxnEs'] = costfxnEs d['costfxnFs'] = costfxnFs return data def plot_convergence(logfile, plotfile='convergence.pdf'): """Makes a plot of the convergence of the cost function and its energy and force components. Parameters ---------- logfile : str Name or path to the log file. plotfile : str Name or path to the plot file. """ data = read_trainlog(logfile) # Find if multiple runs contained in data set. d = data['convergence'] steps = range(len(d['steps'])) breaks = [] for index, step in enumerate(d['steps'][1:]): if step < d['steps'][index]: breaks.append(index) # Make plots. fig = pyplot.figure(figsize=(6., 8.)) # Margins, vertical gap, and top-to-bottom ratio of figure. lm, rm, bm, tm, vg, tb = 0.12, 0.05, 0.08, 0.03, 0.08, 4. bottomaxheight = (1. - bm - tm - vg) / (tb + 1.) ax = fig.add_axes((lm, bm + bottomaxheight + vg, 1. - lm - rm, tb * bottomaxheight)) ax.semilogy(steps, d['es'], 'b', lw=2, label='energy rmse') ax.semilogy(steps, d['emrs'], 'b:', lw=2, label='energy maxresid') if d['force_rmse']: ax.semilogy(steps, d['fs'], 'g', lw=2, label='force rmse') ax.semilogy(steps, d['fmrs'], 'g:', lw=2, label='force maxresid') ax.semilogy(steps, d['costfxns'], color='0.5', lw=2, label='loss function') # Targets. if d['energy_rmse']: ax.semilogy([steps[0], steps[-1]], [d['energy_rmse']] * 2, color='b', linestyle='-', alpha=0.5) if d['energy_maxresid']: ax.semilogy([steps[0], steps[-1]], [d['energy_maxresid']] * 2, color='b', linestyle=':', alpha=0.5) if d['force_rmse']: ax.semilogy([steps[0], steps[-1]], [d['force_rmse']] * 2, color='g', linestyle='-', alpha=0.5) if d['force_maxresid']: ax.semilogy([steps[0], steps[-1]], [d['force_maxresid']] * 2, color='g', linestyle=':', alpha=0.5) ax.set_ylabel('error') ax.legend(loc='best', fontsize=9.) if len(breaks) > 0: ylim = ax.get_ylim() for b in breaks: ax.plot([b] * 2, ylim, '--k') if d['force_rmse']: # Loss function component plot. axf = fig.add_axes((lm, bm, 1. - lm - rm, bottomaxheight)) axf.fill_between(x=np.array(steps), y1=d['costfxnEs'], color='blue') axf.fill_between(x=np.array(steps), y1=d['costfxnEs'], y2=np.array(d['costfxnEs']) + np.array(d['costfxnFs']), color='green') axf.set_ylabel('loss function component') axf.set_xlabel('loss function call') axf.set_ylim(0, 1) else: ax.set_xlabel('loss function call') fig.savefig(plotfile) pyplot.close(fig) andrewpeterson-amp-4878fc892f2c/amp/descriptor/000077500000000000000000000000001332417112400214015ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/descriptor/Makefile000066400000000000000000000000711332417112400230370ustar00rootroot00000000000000cutoffs.mod: gfortran -c cutoffs.f90 cp cutoffs.mod .. andrewpeterson-amp-4878fc892f2c/amp/descriptor/__init__.py000066400000000000000000000001351332417112400235110ustar00rootroot00000000000000#!/usr/bin/env python """ Folder that contains different local environment descriptors. """ andrewpeterson-amp-4878fc892f2c/amp/descriptor/analysis.py000066400000000000000000000077521332417112400236110ustar00rootroot00000000000000import numpy as np from ..utilities import hash_images, get_hash class FingerprintPlot: """Create plots of fingerprint ranges. Initialize with an Amp calculator object. """ def __init__(self, calc): self._calc = calc def __call__(self, images, name='fingerprints.pdf', overlay=None): """Creates a violin plot of fingerprints for each element type in the fed images; saves to specified filename. Optionally, the user can supply either an ase.Atoms or a list of ase.Atom objects with the overlay keyword; this will result in points being added to the fingerprints indicating the values for that atom or atoms object. """ from matplotlib import pyplot from matplotlib.backends.backend_pdf import PdfPages self.compile_fingerprints(images) self.figures = {} for element in self.data.keys(): self.figures[element] = pyplot.figure(figsize=(11., 8.5)) fig = self.figures[element] ax = fig.add_subplot(211) ax.violinplot(self.data[element]) ax.set_ylabel('raw value') ax.set_xlim([0, self.data[element].shape[1] + 1]) if hasattr(self._calc.model.parameters, 'fprange'): ax2 = fig.add_subplot(212) fprange = self._calc.model.parameters.fprange[element] fprange = np.array(fprange) fprange.transpose() d = self.data[element] scaled = ((d - fprange[:, 0]) / (fprange[:, 1] - fprange[:, 0]) * 2.0 - 1.0) ax2.violinplot(scaled) ax2.set_ylabel('scaled value') ax2.set_xlim([0, self.data[element].shape[1] + 1]) ax2.set_ylim([-1.05, 1.05]) ax2.set_xlabel('fingerprint') else: ax.set_xlabel('fingerprint') fig.text(0.5, 0.25, '(No fprange in model; therefore no scaled ' 'fingerprints shown.)', ha='center') fig.text(0.5, 0.95, element, ha='center') if overlay: # Find all atoms. images = [atom.atoms for atom in overlay] images = hash_images(images) self._calc.descriptor.calculate_fingerprints(images) for atom in overlay: key = get_hash(atom.atoms) fingerprints = self._calc.descriptor.fingerprints[key] fingerprint = fingerprints[atom.index] fig = self.figures[fingerprint[0]] ax = fig.axes[0] ax.plot(range(1, len(fingerprint[1]) + 1), fingerprint[1], '.b') fprange = self._calc.model.parameters.fprange[atom.symbol] fprange = np.array(fprange) fprange.transpose() scaled = ((np.array(fingerprint[1]) - fprange[:, 0]) / (fprange[:, 1] - fprange[:, 0]) * 2.0 - 1.0) ax = fig.axes[1] ax.plot(range(1, len(fingerprint[1]) + 1), scaled, '.b') with PdfPages(name) as pdf: for fig in self.figures.values(): pdf.savefig(fig) pyplot.close(fig) def compile_fingerprints(self, images): """Calculates or looks up fingerprints and compiles them, per element, for the images. """ data = self.data = {} images = hash_images(images) self._calc.descriptor.calculate_fingerprints(images) for hash in images.keys(): fingerprints = self._calc.descriptor.fingerprints[hash] for element, fingerprint in fingerprints: if element not in data: data[element] = [] data[element].append(fingerprint) print(element, len(fingerprint)) for element in data.keys(): data[element] = np.array(data[element]) andrewpeterson-amp-4878fc892f2c/amp/descriptor/bispectrum.py000066400000000000000000000622661332417112400241440ustar00rootroot00000000000000import numpy as np from numpy import cos, sqrt, exp from ase.data import atomic_numbers from ase.calculators.calculator import Parameters from ..utilities import Data, Logger, importer from .cutoffs import Cosine, dict2cutoff NeighborList = importer('NeighborList') class Bispectrum(object): """Class that calculates spherical harmonic bispectrum fingerprints. Parameters ---------- cutoff : object or float Cutoff function, typically from amp.descriptor.cutoffs. Can be also fed as a float representing the radius above which neighbor interactions are ignored; in this case a cosine cutoff function will be employed. Default is a 6.5-Angstrom cosine cutoff. Gs : dict Dictionary of symbols and dictionaries for making fingerprints. Either auto-genetrated, or given in the following form, for example: >>> Gs = {"Au": {"Au": 3., "O": 2.}, "O": {"Au": 5., "O": 10.}} jmax : integer or half-integer or dict Maximum degree of spherical harmonics that will be included in the fingerprint vector. Can be also fed as a dictionary with chemical species as keys. dblabel : str Optional separate prefix/location for database files, including fingerprints, fingerprint derivatives, and neighborlists. This file location can be shared between calculator instances to avoid re-calculating redundant information. If not supplied, just uses the value from label. elements : list List of allowed elements present in the system. If not provided, will be found automatically. version : str Version of fingerprints. Raises: ------- RuntimeError, TypeError """ def __init__(self, cutoff=Cosine(6.5), Gs=None, jmax=5, dblabel=None, elements=None, version='2016.02', mode='atom-centered'): # Check of the version of descriptor, particularly if restarting. compatibleversions = ['2016.02', ] if (version is not None) and version not in compatibleversions: raise RuntimeError('Error: Trying to use bispectrum fingerprints' ' version %s, but this module only supports' ' versions %s. You may need an older or ' ' newer version of Amp.' % (version, compatibleversions)) else: version = compatibleversions[-1] # Check that the mode is atom-centered. if mode != 'atom-centered': raise RuntimeError('Bispectrum scheme only works ' 'in atom-centered mode. %s ' 'specified.' % mode) # If the cutoff is provided as a number, Cosine function will be used # by default. if isinstance(cutoff, int) or isinstance(cutoff, float): cutoff = Cosine(cutoff) # If the cutoff is provided as a dictionary, assume we need to load it # with dict2cutoff. if type(cutoff) is dict: cutoff = dict2cutoff(cutoff) # The parameters dictionary contains the minimum information # to produce a compatible descriptor; that is, one that gives # an identical fingerprint when fed an ASE image. p = self.parameters = Parameters( {'importname': '.descriptor.bispectrum.Bispectrum', 'mode': 'atom-centered'}) p.version = version p.cutoff = cutoff.todict() p.Gs = Gs p.jmax = jmax p.elements = elements self.dblabel = dblabel self.parent = None # Can hold a reference to main Amp instance. def tostring(self): """Returns an evaluatable representation of the calculator that can be used to restart the calculator.""" return self.parameters.tostring() def calculate_fingerprints(self, images, parallel=None, log=None, calculate_derivatives=False): """Calculates the fingerpints of the images, for the ones not already done. Parameters ---------- images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. parallel : dict Configuration for parallelization. Should be in same form as in amp.Amp. log : Logger object Write function at which to log data. Note this must be a callable function. calculate_derivatives : bool Decides whether or not fingerprintprimes should also be calculated. """ if parallel is None: parallel = {'cores': 1} if calculate_derivatives is True: import warnings warnings.warn('Zernike descriptor cannot train forces yet. ' 'Force training automatically turnned off. ') calculate_derivatives = False log = Logger(file=None) if log is None else log if (self.dblabel is None) and hasattr(self.parent, 'dblabel'): self.dblabel = self.parent.dblabel self.dblabel = 'amp-data' if self.dblabel is None else self.dblabel p = self.parameters log('Cutoff function: %s' % repr(dict2cutoff(p.cutoff))) if p.elements is None: log('Finding unique set of elements in training data.') p.elements = set([atom.symbol for atoms in images.values() for atom in atoms]) p.elements = sorted(p.elements) log('%i unique elements included: ' % len(p.elements) + ', '.join(p.elements)) log('Maximum degree of spherical harmonic bispectrum:') if isinstance(p.jmax, dict): for _ in p.jmax.keys(): log(' %2s: %d' % (_, p.jmax[_])) else: log('jmax: %d' % p.jmax) if p.Gs is None: log('No coefficient for atomic density function supplied; ' 'creating defaults.') p.Gs = generate_coefficients(p.elements) log('Coefficients of atomic density function for each element:') for _ in p.Gs.keys(): log(' %2s: %s' % (_, str(p.Gs[_]))) # Counts the number of descriptors for each element. no_of_descriptors = {} for element in p.elements: count = 0 if isinstance(p.jmax, dict): for _2j1 in range(int(2 * p.jmax[element]) + 1): for j in range(int(min(_2j1, p.jmax[element])) + 1): count += 1 else: for _2j1 in range(int(2 * p.jmax) + 1): for j in range(int(min(_2j1, p.jmax)) + 1): count += 1 no_of_descriptors[element] = count log('Number of descriptors for each element:') for element in p.elements: log(' %2s: %d' % (element, no_of_descriptors.pop(element))) log('Calculating neighborlists...', tic='nl') if not hasattr(self, 'neighborlist'): calc = NeighborlistCalculator(cutoff=p.cutoff['kwargs']['Rc']) self.neighborlist = Data(filename='%s-neighborlists' % self.dblabel, calculator=calc) self.neighborlist.calculate_items(images, parallel=parallel, log=log) log('...neighborlists calculated.', toc='nl') log('Fingerprinting images...', tic='fp') if not hasattr(self, 'fingerprints'): calc = FingerprintCalculator(neighborlist=self.neighborlist, Gs=p.Gs, jmax=p.jmax, cutoff=p.cutoff,) self.fingerprints = Data(filename='%s-fingerprints' % self.dblabel, calculator=calc) self.fingerprints.calculate_items(images, parallel=parallel, log=log) log('...fingerprints calculated.', toc='fp') # Calculators ################################################################# # Neighborlist Calculator class NeighborlistCalculator: """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. """ def __init__(self, cutoff): self.globals = Parameters({'cutoff': cutoff}) self.keyed = Parameters() self.parallel_command = 'calculate_neighborlists' def calculate(self, image, key): cutoff = self.globals.cutoff n = NeighborList(cutoffs=[cutoff / 2.] * len(image), self_interaction=False, bothways=True, skin=0.) n.update(image) return [n.get_neighbors(index) for index in range(len(image))] class FingerprintCalculator: """For integration with .utilities.Data """ def __init__(self, neighborlist, Gs, jmax, cutoff,): self.globals = Parameters({'cutoff': cutoff, 'Gs': Gs, 'jmax': jmax}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprints' self.factorial = [1] for _ in range(int(3. * jmax) + 2): if _ > 0: self.factorial += [_ * self.factorial[_ - 1]] def calculate(self, image, key): """Makes a list of fingerprints, one per atom, for the fed image. Parameters ---------- image : object ASE atoms object. key : str key of the image after being hashed. """ nl = self.keyed.neighborlist[key] fingerprints = [] for atom in image: symbol = atom.symbol index = atom.index neighbors, offsets = nl[index] neighborsymbols = [image[_].symbol for _ in neighbors] Rs = [image.positions[neighbor] + np.dot(offset, image.cell) for (neighbor, offset) in zip(neighbors, offsets)] self.atoms = image indexfp = self.get_fingerprint(index, symbol, neighborsymbols, Rs) fingerprints.append(indexfp) return fingerprints def get_fingerprint(self, index, symbol, n_symbols, Rs): """Returns the fingerprint of symmetry function values for atom specified by its index and symbol. n_symbols and Rs are lists of neighbors' symbols and Cartesian positions, respectively. Parameters ---------- index : int Index of the center atom. symbol : str Symbol of the center atom. n_symbols : list of str List of neighbors' symbols. Rs : list of list of float List of Cartesian atomic positions of neighbors. Returns ------- symbols, fingerprints : list of float fingerprints for atom specified by its index and symbol. """ home = self.atoms[index].position cutoff = self.globals.cutoff Rc = cutoff['kwargs']['Rc'] jmax = self.globals.jmax if cutoff['name'] == 'Cosine': cutoff_fxn = Cosine(Rc) elif cutoff['name'] == 'Polynomial': # cutoff_fxn = Polynomial(cutoff) raise NotImplementedError() rs = [] psis = [] thetas = [] phis = [] for neighbor in Rs: x = neighbor[0] - home[0] y = neighbor[1] - home[1] z = neighbor[2] - home[2] r = np.linalg.norm(neighbor - home) if r > 10.**(-10.): psi = np.arcsin(r / Rc) theta = np.arccos(z / r) if abs((z / r) - 1.0) < 10.**(-8.): theta = 0.0 elif abs((z / r) + 1.0) < 10.**(-8.): theta = np.pi if x < 0.: phi = np.pi + np.arctan(y / x) elif 0. < x and y < 0.: phi = 2 * np.pi + np.arctan(y / x) elif 0. < x and 0. <= y: phi = np.arctan(y / x) elif x == 0. and 0. < y: phi = 0.5 * np.pi elif x == 0. and y < 0.: phi = 1.5 * np.pi else: phi = 0. rs += [r] psis += [psi] thetas += [theta] phis += [phi] fingerprint = [] for _2j1 in range(int(2 * jmax) + 1): j1 = 0.5 * _2j1 j2 = 0.5 * _2j1 for j in range(int(min(_2j1, jmax)) + 1): value = calculate_B(j1, j2, 1.0 * j, self.globals.Gs[symbol], Rc, cutoff['name'], self.factorial, n_symbols, rs, psis, thetas, phis) value = value.real fingerprint.append(value) return symbol, fingerprint # Auxiliary functions ######################################################### def calculate_B(j1, j2, j, G_element, cutoff, cutofffn, factorial, n_symbols, rs, psis, thetas, phis): """Calculates bi-spectrum B_{j1, j2, j} according to Eq. (5) of "Gaussian Approximation Potentials: The Accuracy of Quantum Mechanics, without the Electrons", Phys. Rev. Lett. 104, 136403. """ mvals = m_values(j) B = 0. for m in mvals: for mp in mvals: c = calculate_c(j, mp, m, G_element, cutoff, cutofffn, factorial, n_symbols, rs, psis, thetas, phis) m1bound = min(j1, m + j2) mp1bound = min(j1, mp + j2) m1 = max(-j1, m - j2) while m1 < (m1bound + 0.5): mp1 = max(-j1, mp - j2) while mp1 < (mp1bound + 0.5): c1 = calculate_c(j1, mp1, m1, G_element, cutoff, cutofffn, factorial, n_symbols, rs, psis, thetas, phis) c2 = calculate_c(j2, mp - mp1, m - m1, G_element, cutoff, cutofffn, factorial, n_symbols, rs, psis, thetas, phis) B += CG(j1, m1, j2, m - m1, j, m, factorial) * \ CG(j1, mp1, j2, mp - mp1, j, mp, factorial) * \ np.conjugate(c) * c1 * c2 mp1 += 1. m1 += 1. return B ############################################################################### def calculate_c(j, mp, m, G_element, cutoff, cutofffn, factorial, n_symbols, rs, psis, thetas, phis): """Calculates c^{j}_{m'm} according to Eq. (4) of "Gaussian Approximation Potentials: The Accuracy of Quantum Mechanics, without the Electrons", Phys. Rev. Lett. 104, 136403 """ if cutofffn is 'Cosine': cutoff_fxn = Cosine(cutoff) elif cutofffn is 'Polynomial': # cutoff_fxn = Polynomial(cutoff) raise NotImplementedError value = 0. for n_symbol, r, psi, theta, phi in zip(n_symbols, rs, psis, thetas, phis): value += G_element[n_symbol] * \ np.conjugate(U(j, m, mp, psi, theta, phi, factorial)) * \ cutoff_fxn(r) return value ############################################################################### def m_values(j): """Returns a list of m values for a given j.""" assert j >= 0, '2*j should be a non-negative integer.' return [j - i for i in range(int(2 * j + 1))] ############################################################################### def binomial(n, k, factorial): """Returns C(n,k) = n!/(k!(n-k)!).""" assert n >= 0 and k >= 0 and n >= k, \ 'n and k should be non-negative integers with n >= k.' c = factorial[int(n)] / (factorial[int(k)] * factorial[int(n - k)]) return c ############################################################################### def WignerD(j, m, mp, alpha, beta, gamma, factorial): """Returns the Wigner-D matrix. alpha, beta, and gamma are the Euler angles.""" result = 0 if abs(beta - np.pi / 2.) < 10.**(-10.): # Varshalovich Eq. (5), Section 4.16, Page 113. # j, m, and mp here are J, M, and M', respectively, in Eq. (5). for k in range(int(2 * j + 1)): if k > j + mp or k > j - m: break elif k < mp - m: continue result += (-1)**k * binomial(j + mp, k, factorial) * \ binomial(j - mp, k + m - mp, factorial) result *= (-1)**(m - mp) * \ sqrt(float(factorial[int(j + m)] * factorial[int(j - m)]) / float((factorial[int(j + mp)] * factorial[int(j - mp)]))) / \ 2.**j result *= exp(-1j * m * alpha) * exp(-1j * mp * gamma) else: # Varshalovich Eq. (10), Section 4.16, Page 113. # m, mpp, and mp here are M, m, and M', respectively, in Eq. (10). mvals = m_values(j) for mpp in mvals: # temp1 = WignerD(j, m, mpp, 0, np.pi/2, 0) = d(j, m, mpp, np.pi/2) temp1 = 0. for k in range(int(2 * j + 1)): if k > j + mpp or k > j - m: break elif k < mpp - m: continue temp1 += (-1)**k * binomial(j + mpp, k, factorial) * \ binomial(j - mpp, k + m - mpp, factorial) temp1 *= (-1)**(m - mpp) * \ sqrt(float(factorial[int(j + m)] * factorial[int(j - m)]) / float((factorial[int(j + mpp)] * factorial[int(j - mpp)]))) / 2.**j # temp2 = WignerD(j, mpp, mp, 0, np.pi/2, 0) = d(j, mpp, mp, # np.pi/2) temp2 = 0. for k in range(int(2 * j + 1)): if k > j - mp or k > j - mpp: break elif k < - mp - mpp: continue temp2 += (-1)**k * binomial(j - mp, k, factorial) * \ binomial(j + mp, k + mpp + mp, factorial) temp2 *= (-1)**(mpp + mp) * \ sqrt(float(factorial[int(j + mpp)] * factorial[int(j - mpp)]) / float((factorial[int(j - mp)] * factorial[int(j + mp)]))) / 2.**j result += temp1 * exp(-1j * mpp * beta) * temp2 # Empirical normalization factor so results match Varshalovich # Tables 4.3-4.12 # Note that this exact normalization does not follow from the # above equations result *= (1j**(2 * j - m - mp)) * ((-1)**(2 * m)) result *= exp(-1j * m * alpha) * exp(-1j * mp * gamma) return result ############################################################################### def U(j, m, mp, omega, theta, phi, factorial): """Calculates rotation matrix U_{MM'}^{J} in terms of rotation angle omega as well as rotation axis angles theta and phi, according to Varshalovich, Eq. (3), Section 4.5, Page 81. j, m, mp, and mpp here are J, M, M', and M'' in Eq. (3). """ result = 0. mvals = m_values(j) for mpp in mvals: result += WignerD(j, m, mpp, phi, theta, -phi, factorial) * \ exp(- 1j * mpp * omega) * \ WignerD(j, mpp, mp, phi, -theta, -phi, factorial) return result ############################################################################### def CG(a, alpha, b, beta, c, gamma, factorial): """Clebsch-Gordan coefficient C_{a alpha b beta}^{c gamma} is calculated acoording to the expression given in Varshalovich Eq. (3), Section 8.2, Page 238.""" if int(2. * a) != 2. * a or int(2. * b) != 2. * b or int(2. * c) != 2. * c: raise ValueError("j values must be integer or half integer") if int(2. * alpha) != 2. * alpha or int(2. * beta) != 2. * beta or \ int(2. * gamma) != 2. * gamma: raise ValueError("m values must be integer or half integer") if alpha + beta - gamma != 0.: return 0. else: minimum = min(a + b - c, a - b + c, -a + b + c, a + b + c + 1., a - abs(alpha), b - abs(beta), c - abs(gamma)) if minimum < 0.: return 0. else: sqrtarg = \ factorial[int(a + alpha)] * \ factorial[int(a - alpha)] * \ factorial[int(b + beta)] * \ factorial[int(b - beta)] * \ factorial[int(c + gamma)] * \ factorial[int(c - gamma)] * \ (2. * c + 1.) * \ factorial[int(a + b - c)] * \ factorial[int(a - b + c)] * \ factorial[int(-a + b + c)] / \ factorial[int(a + b + c + 1.)] sqrtres = sqrt(sqrtarg) zmin = max(a + beta - c, b - alpha - c, 0.) zmax = min(b + beta, a - alpha, a + b - c) sumres = 0. for z in range(int(zmin), int(zmax) + 1): value = \ factorial[int(z)] * \ factorial[int(a + b - c - z)] * \ factorial[int(a - alpha - z)] * \ factorial[int(b + beta - z)] * \ factorial[int(c - b + alpha + z)] * \ factorial[int(c - a - beta + z)] sumres += (-1.)**z / value result = sqrtres * sumres return result ############################################################################### def generate_coefficients(elements): """Automatically generates coefficients if not given by the user. Parameters --------- elements : list of str List of symbols of all atoms. Returns ------- G : dict of dicts """ _G = {} for element in elements: _G[element] = atomic_numbers[element] G = {} for element in elements: G[element] = _G return G ############################################################################### if __name__ == "__main__": """Directly calling this module; apparently from another node. Calls should come as python -m amp.descriptor.example id hostname:port This session will then start a zmq session with that socket, labeling itself with id. Instructions on what to do will come from the socket. """ import sys import tempfile import zmq from ..utilities import MessageDictionary hostsocket = sys.argv[-1] proc_id = sys.argv[-2] msg = MessageDictionary(proc_id) # Send standard lines to stdout signaling process started and where # error is directed. This should be caught by pxssh. (This could # alternatively be done by zmq, but this works.) print('') # Signal that program started. sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('Log and error written to %s' % sys.stderr.name) # Establish client session via zmq; find purpose. context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect('tcp://%s' % hostsocket) socket.send_pyobj(msg('')) purpose = socket.recv_pyobj() if purpose == 'calculate_neighborlists': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() # sys.stderr.write(str(images)) # Just to see if they are there. # Perform the calculations. calc = NeighborlistCalculator(cutoff=cutoff) neighborlist = {} # for key in images.iterkeys(): while len(images) > 0: key, image = images.popitem() # Reduce memory. neighborlist[key] = calc.calculate(image, key) # Send the results. socket.send_pyobj(msg('', neighborlist)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprints': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'Gs')) Gs = socket.recv_pyobj() socket.send_pyobj(msg('', 'jmax')) jmax = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() calc = FingerprintCalculator(neighborlist, Gs, jmax, cutoff,) result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. else: raise NotImplementedError('purpose %s unknown.' % purpose) andrewpeterson-amp-4878fc892f2c/amp/descriptor/cutoffs.f90000066400000000000000000000040571332417112400234000ustar00rootroot00000000000000 module cutoffs implicit none contains function cutoff_fxn(r, rc, cutofffn, p_gamma) double precision:: r, rc, pi, cutoff_fxn ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn ! To avoid noise, for each call of this function, it is better to ! set returned variables to 0.0d0. cutoff_fxn = 0.0d0 if (cutofffn == 'Cosine') then if (r > rc) then cutoff_fxn = 0.0d0 else pi = 4.0d0 * datan(1.0d0) cutoff_fxn = 0.5d0 * (cos(pi*r/rc) + 1.0d0) end if elseif (cutofffn == 'Polynomial') then if (r > rc) then cutoff_fxn = 0.0d0 else cutoff_fxn = 1. + p_gamma & * (r / rc) ** (p_gamma + 1) & - (p_gamma + 1) * (r / rc) ** p_gamma end if endif end function cutoff_fxn function cutoff_fxn_prime(r, rc, cutofffn, p_gamma) double precision:: r, rc, cutoff_fxn_prime, pi ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn ! To avoid noise, for each call of this function, it is better to ! set returned variables to 0.0d0. cutoff_fxn_prime = 0.0d0 if (cutofffn == 'Cosine') then if (r > rc) then cutoff_fxn_prime = 0.0d0 else pi = 4.0d0 * datan(1.0d0) cutoff_fxn_prime = -0.5d0 * pi * sin(pi*r/rc) / rc end if elseif (cutofffn == 'Polynomial') then if (r > rc) then cutoff_fxn_prime = 0.0d0 else cutoff_fxn_prime = (p_gamma * (p_gamma + 1) / rc) & * ((r / rc) ** p_gamma - (r / rc) ** (p_gamma - 1)) end if end if end function cutoff_fxn_prime end module cutoffs andrewpeterson-amp-4878fc892f2c/amp/descriptor/cutoffs.py000066400000000000000000000074311332417112400234310ustar00rootroot00000000000000#!/usr/bin/env python """ This script contains different cutoff function forms. Note all cutoff functions need to have a "todict" method to support saving/loading as an Amp object. All cutoff functions also need to have an `Rc` attribute which is the maximum distance at which properties are calculated; this will be used in calculating neighborlists. """ import numpy as np def dict2cutoff(dct): """This function converts a dictionary (which was created with the to_dict method of one of the cutoff classes) into an instantiated version of the class. Modeled after ASE's dict2constraint function. """ if len(dct) != 2: raise RuntimeError('Cutoff dictionary must have only two values,' ' "name" and "kwargs".') return globals()[dct['name']](**dct['kwargs']) class Cosine(object): """Cosine functional form suggested by Behler. Parameters --------- Rc : float Radius above which neighbor interactions are ignored. """ def __init__(self, Rc): self.Rc = Rc def __call__(self, Rij): """ Parameters ---------- Rij : float Distance between pair atoms. Returns ------- float The value of the cutoff function. """ if Rij > self.Rc: return 0. else: return 0.5 * (np.cos(np.pi * Rij / self.Rc) + 1.) def prime(self, Rij): """Derivative of the Cosine cutoff function. Parameters ---------- Rij : float Distance between pair atoms. Returns ------- float The value of derivative of the cutoff function. """ if Rij > self.Rc: return 0. else: return -0.5 * np.pi / self.Rc * np.sin(np.pi * Rij / self.Rc) def todict(self): return {'name': 'Cosine', 'kwargs': {'Rc': self.Rc}} def __repr__(self): return ('' % self.Rc) class Polynomial(object): """Polynomial functional form suggested by Khorshidi and Peterson. Parameters ---------- gamma : float The power of polynomial. Rc : float Radius above which neighbor interactions are ignored. """ def __init__(self, Rc, gamma=4): self.gamma = gamma self.Rc = Rc def __call__(self, Rij): """ Parameters ---------- Rij : float Distance between pair atoms. Returns ------- value : float The value of the cutoff function. """ if Rij > self.Rc: return 0. else: value = 1. + self.gamma * (Rij / self.Rc) ** (self.gamma + 1) - \ (self.gamma + 1) * (Rij / self.Rc) ** self.gamma return value def prime(self, Rij): """Derivative of the Polynomial cutoff function. Parameters ---------- Rij : float Distance between pair atoms. Returns ------- float The value of derivative of the cutoff function. """ if Rij > self.Rc: return 0. else: value = (self.gamma * (self.gamma + 1) / self.Rc) * \ ((Rij / self.Rc) ** self.gamma - (Rij / self.Rc) ** (self.gamma - 1)) return value def todict(self): return {'name': 'Polynomial', 'kwargs': {'Rc': self.Rc, 'gamma': self.gamma } } def __repr__(self): return ('' % (self.Rc, self.gamma)) andrewpeterson-amp-4878fc892f2c/amp/descriptor/example.py000066400000000000000000000312341332417112400234110ustar00rootroot00000000000000import time import numpy as np from ase.calculators.calculator import Parameters from ..utilities import Data, Logger, importer from .cutoffs import Cosine NeighborList = importer('NeighborList') class AtomCenteredExample(object): """Class that calculates fingerprints. This is an example class that doesn't do much; it just shows the code structure. If making your own module, you can copy and modify this one. Parameters ---------- cutoff : object or float Cutoff function. Can be also fed as a float representing the radius above which neighbor interactions are ignored. Default is 6.5 Angstroms. anotherparameter : float Just an example. dblabel : str Optional separate prefix/location for database files, including fingerprints, fingerprint derivatives, and neighborlists. This file location can be shared between calculator instances to avoid re-calculating redundant information. If not supplied, just uses the value from label. elements : list List of allowed elements present in the system. If not provided, will be found automatically. version : str Version of fingerprints. Raises ------ RuntimeError, TypeError """ def __init__(self, cutoff=Cosine(6.5), anotherparameter=12.2, dblabel=None, elements=None, version=None, mode='atom-centered'): # Check of the version of descriptor, particularly if restarting. compatibleversions = ['2016.02', ] if (version is not None) and version not in compatibleversions: raise RuntimeError('Error: Trying to use Example fingerprints' ' version %s, but this module only supports' ' versions %s. You may need an older or ' ' newer version of Amp.' % (version, compatibleversions)) else: version = compatibleversions[-1] # Check that the mode is atom-centered. if mode != 'atom-centered': raise RuntimeError('This scheme only works ' 'in atom-centered mode. %s ' 'specified.' % mode) # If the cutoff is provided as a number, Cosine function will be used # by default. if isinstance(cutoff, int) or isinstance(cutoff, float): cutoff = Cosine(cutoff) # The parameters dictionary contains the minimum information # to produce a compatible descriptor; that is, one that gives # an identical fingerprint when fed an ASE image. p = self.parameters = Parameters( {'importname': '.descriptor.example.AtomCenteredExample', 'mode': 'atom-centered'}) p.version = version p.cutoff = cutoff.Rc p.cutofffn = cutoff.__class__.__name__ p.anotherparameter = anotherparameter p.elements = elements self.dblabel = dblabel self.parent = None # Can hold a reference to main Amp instance. def tostring(self): """Returns an evaluatable representation of the calculator that can be used to restart the calculator.""" return self.parameters.tostring() def calculate_fingerprints(self, images, parallel=None, log=None, calculate_derivatives=False): """Calculates the fingerpints of the images, for the ones not already done. Parameters ---------- images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. parallel : dict Configuration for parallelization. Should be in same form as in amp.Amp. log : Logger object Write function at which to log data. Note this must be a callable function. calculate_derivatives : bool Decides whether or not fingerprintprimes should also be calculated. """ if parallel is None: parallel = {'cores': 1} log = Logger(file=None) if log is None else log if (self.dblabel is None) and hasattr(self.parent, 'dblabel'): self.dblabel = self.parent.dblabel self.dblabel = 'amp-data' if self.dblabel is None else self.dblabel p = self.parameters log('Cutoff radius: %.2f' % p.cutoff) log('Cutoff function: %s' % p.cutofffn) if p.elements is None: log('Finding unique set of elements in training data.') p.elements = set([atom.symbol for atoms in images.values() for atom in atoms]) p.elements = sorted(p.elements) log('%i unique elements included: ' % len(p.elements) + ', '.join(p.elements)) log('anotherparameter: %.3f' % p.anotherparameter) log('Calculating neighborlists...', tic='nl') if not hasattr(self, 'neighborlist'): calc = NeighborlistCalculator(cutoff=p.cutoff) self.neighborlist = Data(filename='%s-neighborlists' % self.dblabel, calculator=calc) self.neighborlist.calculate_items(images, parallel=parallel, log=log) log('...neighborlists calculated.', toc='nl') log('Fingerprinting images...', tic='fp') if not hasattr(self, 'fingerprints'): calc = FingerprintCalculator(neighborlist=self.neighborlist, anotherparamter=p.anotherparameter, cutoff=p.cutoff, cutofffn=p.cutofffn) self.fingerprints = Data(filename='%s-fingerprints' % self.dblabel, calculator=calc) self.fingerprints.calculate_items(images, parallel=parallel, log=log) log('...fingerprints calculated.', toc='fp') # Calculators ################################################################# # Neighborlist Calculator class NeighborlistCalculator: """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- cutoff : float Radius above which neighbor interactions are ignored. """ def __init__(self, cutoff): self.globals = Parameters({'cutoff': cutoff}) self.keyed = Parameters() self.parallel_command = 'calculate_neighborlists' def calculate(self, image, key): """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- image : object ASE atoms object. key : str key of the image after being hashed. """ cutoff = self.globals.cutoff n = NeighborList(cutoffs=[cutoff / 2.] * len(image), self_interaction=False, bothways=True, skin=0.) n.update(image) return [n.get_neighbors(index) for index in range(len(image))] class FingerprintCalculator: """For integration with .utilities.Data""" def __init__(self, neighborlist, anotherparamter, cutoff, cutofffn): self.globals = Parameters({'cutoff': cutoff, 'cutofffn': cutofffn, 'anotherparameter': anotherparamter}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprints' def calculate(self, image, key): """Makes a list of fingerprints, one per atom, for the fed image. """ nl = self.keyed.neighborlist[key] fingerprints = [] for atom in image: symbol = atom.symbol index = atom.index neighbors, offsets = nl[index] neighborsymbols = [image[_].symbol for _ in neighbors] Rs = [image.positions[neighbor] + np.dot(offset, image.cell) for (neighbor, offset) in zip(neighbors, offsets)] self.atoms = image indexfp = self.get_fingerprint(index, symbol, neighborsymbols, Rs) fingerprints.append(indexfp) return fingerprints def get_fingerprint(self, index, symbol, n_symbols, Rs): """ Returns the fingerprint of symmetry function values for atom specified by its index and symbol. n_symbols and Rs are lists of neighbors' symbols and Cartesian positions, respectively. This function doesn't actually do anything but sleep and return a vector of ones. Parameters ---------- index : int index: Index of the center atom. symbol: str Symbol of the center atom. n_symbols: list of str List of neighbors' symbols. Rs: list of list of float List of Cartesian atomic positions. Returns ------- symbols, fingerprints : list of float Fingerprints for atom specified by its index and symbol. """ time.sleep(1.0) # Pretend to do some work. fingerprint = [1., 1., 1., 1.] return symbol, fingerprint if __name__ == "__main__": """Directly calling this module; apparently from another node. Calls should come as python -m amp.descriptor.example id hostname:port This session will then start a zmq session with that socket, labeling itself with id. Instructions on what to do will come from the socket. """ import sys import tempfile import zmq from ..utilities import MessageDictionary hostsocket = sys.argv[-1] proc_id = sys.argv[-2] msg = MessageDictionary(proc_id) # Send standard lines to stdout signaling process started and where # error is directed. This should be caught by pxssh. (This could # alternatively be done by zmq, but this works.) print('') # Signal that program started. sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('Log and error written to %s' % sys.stderr.name) # Establish client session via zmq; find purpose. context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect('tcp://%s' % hostsocket) socket.send_pyobj(msg('')) purpose = socket.recv_pyobj() if purpose == 'calculate_neighborlists': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() # sys.stderr.write(str(images)) # Just to see if they are there. # Perform the calculations. calc = NeighborlistCalculator(cutoff=cutoff) neighborlist = {} # for key in images.iterkeys(): while len(images) > 0: key, image = images.popitem() # Reduce memory. neighborlist[key] = calc.calculate(image, key) # Send the results. socket.send_pyobj(msg('', neighborlist)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprints': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'cutofffn')) cutofffn = socket.recv_pyobj() socket.send_pyobj(msg('', 'anotherparameter')) anotherparameter = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() calc = FingerprintCalculator(neighborlist, anotherparameter, cutoff, cutofffn) result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. else: raise NotImplementedError('purpose %s unknown.' % purpose) andrewpeterson-amp-4878fc892f2c/amp/descriptor/gaussian.f90000066400000000000000000000514631332417112400235440ustar00rootroot00000000000000!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine calculate_g2(neighbornumbers, neighborpositions, & g_number, g_eta, p_gamma, rc, cutofffn, ri, num_neighbors, ridge) use cutoffs implicit none integer, dimension(num_neighbors):: neighbornumbers integer, dimension(1):: g_number double precision, dimension(num_neighbors, 3):: & neighborpositions double precision, dimension(3):: ri integer:: num_neighbors double precision:: g_eta, rc ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn double precision:: ridge !f2py intent(in):: neighbornumbers, neighborpositions, g_number !f2py intent(in):: g_eta, rc, ri, p_gamma !f2py intent(hide):: num_neighbors !f2py intent(out):: ridge integer:: j, match, xyz double precision, dimension(3):: Rij_vector double precision:: Rij, term ridge = 0.0d0 do j = 1, num_neighbors match = compare(neighbornumbers(j), g_number(1)) if (match == 1) then do xyz = 1, 3 Rij_vector(xyz) = & neighborpositions(j, xyz) - ri(xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) term = exp(-g_eta*(Rij**2.0d0) / (rc ** 2.0d0)) if (present(p_gamma)) then term = term * cutoff_fxn(Rij, rc, & cutofffn, p_gamma) else term = term * cutoff_fxn(Rij, rc, cutofffn) endif ridge = ridge + term end if end do CONTAINS function compare(try, val) result(match) ! Returns 1 if try is the same set as val, 0 if not. implicit none integer, intent(in):: try, val integer:: match if (try == val) then match = 1 else match = 0 end if end function compare end subroutine calculate_g2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine calculate_g4(neighbornumbers, neighborpositions, & g_numbers, g_gamma, g_zeta, g_eta, rc, cutofffn, ri, & num_neighbors, ridge, p_gamma) use cutoffs implicit none integer, dimension(num_neighbors):: neighbornumbers integer, dimension(2):: g_numbers double precision, dimension(num_neighbors, 3):: & neighborpositions double precision, dimension(3):: ri integer:: num_neighbors double precision:: g_gamma, g_zeta, g_eta, rc ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn double precision:: ridge !f2py intent(in):: neighbornumbers, neighborpositions !f2py intent(in):: g_numbers, g_gamma, g_zeta !f2py intent(in):: g_eta, rc, ri, p_gamma !f2py intent(hide):: num_neighbors !f2py intent(out):: ridge integer:: j, k, match, xyz double precision, dimension(3):: Rij_vector, Rik_vector double precision, dimension(3):: Rjk_vector double precision:: Rij, Rik, Rjk, costheta, term ridge = 0.0d0 do j = 1, num_neighbors do k = (j + 1), num_neighbors match = compare(neighbornumbers(j), & neighbornumbers(k), g_numbers(1), g_numbers(2)) if (match == 1) then do xyz = 1, 3 Rij_vector(xyz) = & neighborpositions(j, xyz) - ri(xyz) Rik_vector(xyz) = & neighborpositions(k, xyz) - ri(xyz) Rjk_vector(xyz) = & neighborpositions(k, xyz) - & neighborpositions(j, xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) Rik = sqrt(dot_product(Rik_vector, Rik_vector)) Rjk = sqrt(dot_product(Rjk_vector, Rjk_vector)) costheta = & dot_product(Rij_vector, Rik_vector) / Rij / Rik term = (1.0d0 + g_gamma * costheta)**g_zeta term = term*& exp(-g_eta*(Rij**2 + Rik**2 + Rjk**2)& /(rc ** 2.0d0)) if (present(p_gamma)) then term = term*cutoff_fxn(Rij, rc, cutofffn, & p_gamma) term = term*cutoff_fxn(Rik, rc, cutofffn, & p_gamma) term = term*cutoff_fxn(Rjk, rc, cutofffn, & p_gamma) else term = term*cutoff_fxn(Rij, rc, cutofffn) term = term*cutoff_fxn(Rik, rc, cutofffn) term = term*cutoff_fxn(Rjk, rc, cutofffn) endif ridge = ridge + term end if end do end do ridge = ridge * 2.0d0**(1.0d0 - g_zeta) CONTAINS function compare(try1, try2, val1, val2) result(match) ! Returns 1 if (try1, try2) is the same set as (val1, val2), 0 if not. implicit none integer, intent(in):: try1, try2, val1, val2 integer:: match integer:: ntry1, ntry2, nval1, nval2 ! First sort to avoid endless logical loops. if (try1 < try2) then ntry1 = try1 ntry2 = try2 else ntry1 = try2 ntry2 = try1 end if if (val1 < val2) then nval1 = val1 nval2 = val2 else nval1 = val2 nval2 = val1 end if if (ntry1 == nval1 .AND. ntry2 == nval2) then match = 1 else match = 0 end if end function compare end subroutine calculate_g4 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine calculate_g2_prime(neighborindices, neighbornumbers, & neighborpositions, g_number, g_eta, rc, cutofffn, i, ri, m, l, & num_neighbors, ridge, p_gamma) use cutoffs implicit none integer, dimension(num_neighbors):: neighborindices integer, dimension(num_neighbors):: neighbornumbers integer, dimension(1):: g_number double precision, dimension(num_neighbors, 3):: & neighborpositions double precision, dimension(3):: ri, Rj integer:: num_neighbors, m, l, i double precision:: g_eta, rc ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn double precision:: ridge !f2py intent(in):: neighborindices, neighbornumbers !f2py intent(in):: neighborpositions, g_number !f2py intent(in):: g_eta, rc, i, ri, m, l, p_gamma !f2py intent(hide):: num_neighbors !f2py intent(out):: ridge integer:: j, match, xyz double precision, dimension(3):: Rij_vector double precision:: Rij, term1, dRijdRml ridge = 0.0d0 do j = 1, num_neighbors match = compare(neighbornumbers(j), g_number(1)) if (match == 1) then do xyz = 1, 3 Rj(xyz) = neighborpositions(j, xyz) Rij_vector(xyz) = Rj(xyz) - ri(xyz) end do dRijdRml = & dRij_dRml(i, neighborindices(j), ri, Rj, m, l) if (dRijdRml /= 0.0d0) then Rij = sqrt(dot_product(Rij_vector, Rij_vector)) if (present(p_gamma)) then term1 = - 2.0d0 * g_eta * Rij * & cutoff_fxn(Rij, rc, cutofffn, p_gamma) / & (rc ** 2.0d0) + cutoff_fxn_prime(Rij, rc, & cutofffn, p_gamma) else term1 = - 2.0d0 * g_eta * Rij * & cutoff_fxn(Rij, rc, cutofffn) / & (rc ** 2.0d0) + cutoff_fxn_prime(Rij, rc, & cutofffn) endif ridge = ridge + exp(- g_eta * (Rij**2.0d0) / & (rc ** 2.0d0)) * term1 * dRijdRml end if end if end do CONTAINS function compare(try, val) result(match) ! Returns 1 if try is the same set as val, 0 if not. implicit none integer, intent(in):: try, val integer:: match if (try == val) then match = 1 else match = 0 end if end function compare function dRij_dRml(i, j, Ri, Rj, m, l) integer i, j, m, l double precision, dimension(3):: Ri, Rj, Rij_vector double precision:: dRij_dRml, Rij do xyz = 1, 3 Rij_vector(xyz) = Rj(xyz) - Ri(xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) if ((m == i) .AND. (i /= j)) then dRij_dRml = - (Rj(l + 1) - Ri(l + 1)) / Rij else if ((m == j) .AND. (i /= j)) then dRij_dRml = (Rj(l + 1) - Ri(l + 1)) / Rij else dRij_dRml = 0.0d0 end if end function end subroutine calculate_g2_prime !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine calculate_g4_prime(neighborindices, neighbornumbers, & neighborpositions, g_numbers, g_gamma, g_zeta, g_eta, rc, & cutofffn, i, ri, m, l, num_neighbors, ridge, p_gamma) use cutoffs implicit none integer, dimension(num_neighbors):: neighborindices integer, dimension(num_neighbors):: neighbornumbers integer, dimension(2):: g_numbers double precision, dimension(num_neighbors, 3):: & neighborpositions double precision, dimension(3):: ri, Rj, Rk integer:: num_neighbors, i, m, l double precision:: g_gamma, g_zeta, g_eta, rc ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn double precision:: ridge !f2py intent(in):: neighbornumbers, neighborpositions !f2py intent(in):: g_numbers, g_gamma, g_zeta, p_gamma !f2py intent(in):: g_eta, rc, ri, neighborindices , i, m, l !f2py intent(hide):: num_neighbors !f2py intent(out):: ridge integer:: j, k, match, xyz double precision, dimension(3):: Rij_vector, Rik_vector double precision, dimension(3):: Rjk_vector double precision:: Rij, Rik, Rjk, costheta double precision:: c1, fcRij, fcRik, fcRjk double precision:: fcRijfcRikfcRjk, dCosthetadRml double precision:: dRijdRml, dRikdRml, dRjkdRml double precision:: term1, term2, term3, term4, term5 double precision:: term6 ridge = 0.0d0 do j = 1, num_neighbors do k = (j + 1), num_neighbors match = compare(neighbornumbers(j), & neighbornumbers(k), g_numbers(1), g_numbers(2)) if (match == 1) then do xyz = 1, 3 Rj(xyz) = neighborpositions(j, xyz) Rk(xyz) = neighborpositions(k, xyz) Rij_vector(xyz) = Rj(xyz) - ri(xyz) Rik_vector(xyz) = Rk(xyz) - ri(xyz) Rjk_vector(xyz) = Rk(xyz) - Rj(xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) Rik = sqrt(dot_product(Rik_vector, Rik_vector)) Rjk = sqrt(dot_product(Rjk_vector, Rjk_vector)) costheta = & dot_product(Rij_vector, Rik_vector) / Rij / Rik c1 = (1.0d0 + g_gamma * costheta) if (present(p_gamma)) then fcRij = cutoff_fxn(Rij, rc, cutofffn, p_gamma) fcRik = cutoff_fxn(Rik, rc, cutofffn, p_gamma) fcRjk = cutoff_fxn(Rjk, rc, cutofffn, p_gamma) else fcRij = cutoff_fxn(Rij, rc, cutofffn) fcRik = cutoff_fxn(Rik, rc, cutofffn) fcRjk = cutoff_fxn(Rjk, rc, cutofffn) endif if (g_zeta == 1.0d0) then term1 = exp(-g_eta*(Rij**2 + Rik**2 + Rjk**2)& / (rc ** 2.0d0)) else term1 = (c1**(g_zeta - 1.0d0)) & * exp(-g_eta*(Rij**2 + Rik**2 + Rjk**2)& / (rc ** 2.0d0)) end if term2 = 0.d0 fcRijfcRikfcRjk = fcRij * fcRik * fcRjk dCosthetadRml = & dCos_ijk_dR_ml(i, neighborindices(j), & neighborindices(k), ri, Rj, Rk, m, l) if (dCosthetadRml /= 0.d0) then term2 = term2 + g_gamma * g_zeta * dCosthetadRml end if dRijdRml = & dRij_dRml(i, neighborindices(j), ri, Rj, m, l) if (dRijdRml /= 0.0d0) then term2 = & term2 - 2.0d0 * c1 * g_eta * Rij * dRijdRml & / (rc ** 2.0d0) end if dRikdRml = & dRij_dRml(i, neighborindices(k), ri, Rk, m, l) if (dRikdRml /= 0.0d0) then term2 = & term2 - 2.0d0 * c1 * g_eta * Rik * dRikdRml & / (rc ** 2.0d0) end if dRjkdRml = & dRij_dRml(neighborindices(j), neighborindices(k), & Rj, Rk, m, l) if (dRjkdRml /= 0.0d0) then term2 = & term2 - 2.0d0 * c1 * g_eta * Rjk * dRjkdRml & / (rc ** 2.0d0) end if term3 = fcRijfcRikfcRjk * term2 if (present(p_gamma)) then term4 = & cutoff_fxn_prime(Rij, rc, cutofffn, p_gamma) & * dRijdRml * fcRik * fcRjk term5 = & fcRij * cutoff_fxn_prime(Rik, rc, cutofffn, & p_gamma) * dRikdRml * fcRjk term6 = & fcRij * fcRik * cutoff_fxn_prime(Rjk, rc, & cutofffn, p_gamma) * dRjkdRml else term4 = & cutoff_fxn_prime(Rij, rc, cutofffn) & * dRijdRml * fcRik * fcRjk term5 = & fcRij * cutoff_fxn_prime(Rik, rc, cutofffn) & * dRikdRml * fcRjk term6 = & fcRij * fcRik * cutoff_fxn_prime(Rjk, rc, & cutofffn) * dRjkdRml endif ridge = ridge + & term1 * (term3 + c1 * (term4 + term5 + term6)) end if end do end do ridge = ridge * (2.0d0**(1.0d0 - g_zeta)) CONTAINS function compare(try1, try2, val1, val2) result(match) ! Returns 1 if (try1, try2) is the same set as (val1, val2), 0 if not. implicit none integer, intent(in):: try1, try2, val1, val2 integer:: match integer:: ntry1, ntry2, nval1, nval2 ! First sort to avoid endless logical loops. if (try1 < try2) then ntry1 = try1 ntry2 = try2 else ntry1 = try2 ntry2 = try1 end if if (val1 < val2) then nval1 = val1 nval2 = val2 else nval1 = val2 nval2 = val1 end if if (ntry1 == nval1 .AND. ntry2 == nval2) then match = 1 else match = 0 end if end function compare function dRij_dRml(i, j, Ri, Rj, m, l) integer i, j, m, l double precision, dimension(3):: Ri, Rj, Rij_vector double precision:: dRij_dRml, Rij do xyz = 1, 3 Rij_vector(xyz) = Rj(xyz) - Ri(xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) if ((m == i) .AND. (i /= j)) then dRij_dRml = - (Rj(l + 1) - Ri(l + 1)) / Rij else if ((m == j) .AND. (i /= j)) then dRij_dRml = (Rj(l + 1) - Ri(l + 1)) / Rij else dRij_dRml = 0.0d0 end if end function function dCos_ijk_dR_ml(i, j, k, ri, Rj, Rk, m, l) implicit none integer:: i, j, k, m, l double precision:: dCos_ijk_dR_ml double precision, dimension(3):: ri, Rj, Rk integer, dimension(3):: dRijdRml, dRikdRml double precision:: dRijdRml_, dRikdRml_ do xyz = 1, 3 Rij_vector(xyz) = Rj(xyz) - ri(xyz) Rik_vector(xyz) = Rk(xyz) - ri(xyz) end do Rij = sqrt(dot_product(Rij_vector, Rij_vector)) Rik = sqrt(dot_product(Rik_vector, Rik_vector)) dCos_ijk_dR_ml = 0.0d0 dRijdRml = dRij_dRml_vector(i, j, m, l) if ((dRijdRml(1) /= 0) .OR. (dRijdRml(2) /= 0) .OR. & (dRijdRml(3) /= 0)) then dCos_ijk_dR_ml = dCos_ijk_dR_ml + 1.0d0 / (Rij * Rik) * & dot_product(dRijdRml, Rik_vector) end if dRikdRml = dRij_dRml_vector(i, k, m, l) if ((dRikdRml(1) /= 0) .OR. (dRikdRml(2) /= 0) .OR. & (dRikdRml(3) /= 0)) then dCos_ijk_dR_ml = dCos_ijk_dR_ml + 1.0d0 / (Rij * Rik) * & dot_product(dRikdRml, Rij_vector) end if dRijdRml_ = dRij_dRml(i, j, ri, Rj, m, l) if (dRijdRml_ /= 0.0d0) then dCos_ijk_dR_ml = dCos_ijk_dR_ml - 1.0d0 / (Rij * Rij * Rik) * & dot_product(Rij_vector, Rik_vector) * dRijdRml_ end if dRikdRml_ = dRij_dRml(i, k, ri, Rk, m, l) if (dRikdRml_ /= 0.0d0) then dCos_ijk_dR_ml = dCos_ijk_dR_ml - 1.0d0 / (Rij * Rik * Rik) * & dot_product(Rij_vector, Rik_vector) * dRikdRml_ end if end function function dRij_dRml_vector(i, j, m, l) implicit none integer:: i, j, m, l, c1 integer, dimension(3):: dRij_dRml_vector if ((m /= i) .AND. (m /= j)) then dRij_dRml_vector(1) = 0 dRij_dRml_vector(2) = 0 dRij_dRml_vector(3) = 0 else c1 = Kronecker(m, j) - Kronecker(m, i) dRij_dRml_vector(1) = c1 * Kronecker(0, l) dRij_dRml_vector(2) = c1 * Kronecker(1, l) dRij_dRml_vector(3) = c1 * Kronecker(2, l) end if end function function Kronecker(i, j) implicit none integer:: i, j integer:: Kronecker if (i == j) then Kronecker = 1 else Kronecker = 0 end if end function end subroutine calculate_g4_prime !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! andrewpeterson-amp-4878fc892f2c/amp/descriptor/gaussian.py000066400000000000000000001406261332417112400235760ustar00rootroot00000000000000import numpy as np from ase.data import atomic_numbers from ase.calculators.calculator import Parameters from ..utilities import Data, Logger, importer from .cutoffs import Cosine, dict2cutoff NeighborList = importer('NeighborList') try: from .. import fmodules except ImportError: fmodules = None class Gaussian(object): """Class that calculates Gaussian fingerprints (i.e., Behler-style). Parameters ---------- cutoff : object or float Cutoff function, typically from amp.descriptor.cutoffs. Can be also fed as a float representing the radius above which neighbor interactions are ignored; in this case a cosine cutoff function will be employed. Default is a 6.5-Angstrom cosine cutoff. Gs : dict Dictionary of symbols and lists of dictionaries for making symmetry functions. Either auto-genetrated, or given in the following form, for example: >>> Gs = {"O": [{"type":"G2", "element":"O", "eta":10.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":5., "gamma":1., "zeta":1.0}], ... "Au": [{"type":"G2", "element":"O", "eta":2.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":2., "gamma":1., "zeta":5.0}]} dblabel : str Optional separate prefix/location for database files, including fingerprints, fingerprint derivatives, and neighborlists. This file location can be shared between calculator instances to avoid re-calculating redundant information. If not supplied, just uses the value from label. elements : list List of allowed elements present in the system. If not provided, will be found automatically. version : str Version of fingerprints. fortran : bool If True, will use fortran modules, if False, will not. mode : str Can be either 'atom-centered' or 'image-centered'. Raises ------ RuntimeError """ def __init__(self, cutoff=Cosine(6.5), Gs=None, dblabel=None, elements=None, version=None, fortran=True, mode='atom-centered'): # Check of the version of descriptor, particularly if restarting. compatibleversions = ['2015.12', ] if (version is not None) and version not in compatibleversions: raise RuntimeError('Error: Trying to use Gaussian fingerprints' ' version %s, but this module only supports' ' versions %s. You may need an older or ' ' newer version of Amp.' % (version, compatibleversions)) else: version = compatibleversions[-1] # Check that the mode is atom-centered. if mode != 'atom-centered': raise RuntimeError('Gaussian scheme only works ' 'in atom-centered mode. %s ' 'specified.' % mode) # If the cutoff is provided as a number, Cosine function will be used # by default. if isinstance(cutoff, int) or isinstance(cutoff, float): cutoff = Cosine(cutoff) # If the cutoff is provided as a dictionary, assume we need to load it # with dict2cutoff. if type(cutoff) is dict: cutoff = dict2cutoff(cutoff) # The parameters dictionary contains the minimum information # to produce a compatible descriptor; that is, one that gives # an identical fingerprint when fed an ASE image. p = self.parameters = Parameters( {'importname': '.descriptor.gaussian.Gaussian', 'mode': 'atom-centered'}) p.version = version p.cutoff = cutoff.todict() p.Gs = Gs p.elements = elements self.dblabel = dblabel self.fortran = fortran self.parent = None # Can hold a reference to main Amp instance. def tostring(self): """Returns an evaluatable representation of the calculator that can be used to restart the calculator. """ return self.parameters.tostring() def calculate_fingerprints(self, images, parallel=None, log=None, calculate_derivatives=False): """Calculates the fingerpints of the images, for the ones not already done. Parameters ---------- images : dict Dictionary of images; the key is a unique ID assigned to each image and each value is an ASE atoms object. Typically created from amp.utilities.hash_images. parallel : dict Configuration for parallelization. Should be in same form as in amp.Amp. log : Logger object Write function at which to log data. Note this must be a callable function. calculate_derivatives : bool Decides whether or not fingerprintprimes should also be calculated. """ if parallel is None: parallel = {'cores': 1} log = Logger(file=None) if log is None else log if (self.dblabel is None) and hasattr(self.parent, 'dblabel'): self.dblabel = self.parent.dblabel self.dblabel = 'amp-data' if self.dblabel is None else self.dblabel p = self.parameters log('Cutoff function: %s' % repr(dict2cutoff(p.cutoff))) if p.elements is None: log('Finding unique set of elements in training data.') p.elements = set([atom.symbol for atoms in images.values() for atom in atoms]) p.elements = sorted(p.elements) log('%i unique elements included: ' % len(p.elements) + ', '.join(p.elements)) if p.Gs is None: log('No symmetry functions supplied; creating defaults.') p.Gs = make_default_symmetry_functions(p.elements) log('Number of symmetry functions for each element:') for _ in p.Gs.keys(): log(' %2s: %i' % (_, len(p.Gs[_]))) for element, fingerprints in p.Gs.items(): log('{} feature vector functions:'.format(element)) for index, fp in enumerate(fingerprints): if fp['type'] == 'G2': log(' {}: {}, {}, eta = {}' .format(index, fp['type'], fp['element'], fp['eta'])) elif fp['type'] == 'G4': log(' {}: {}, ({}, {}), eta={}, gamma={}, zeta={}' .format(index, fp['type'], fp['elements'][0], fp['elements'][1], fp['eta'], fp['gamma'], fp['zeta'])) else: log(str(fp)) log('Calculating neighborlists...', tic='nl') if not hasattr(self, 'neighborlist'): calc = NeighborlistCalculator(cutoff=p.cutoff['kwargs']['Rc']) self.neighborlist = \ Data(filename='%s-neighborlists' % self.dblabel, calculator=calc) self.neighborlist.calculate_items(images, parallel=parallel, log=log) log('...neighborlists calculated.', toc='nl') log('Fingerprinting images...', tic='fp') if not hasattr(self, 'fingerprints'): calc = FingerprintCalculator(neighborlist=self.neighborlist, Gs=p.Gs, cutoff=p.cutoff, fortran=self.fortran) self.fingerprints = Data(filename='%s-fingerprints' % self.dblabel, calculator=calc) self.fingerprints.calculate_items(images, parallel=parallel, log=log) log('...fingerprints calculated.', toc='fp') if calculate_derivatives: log('Calculating fingerprint derivatives...', tic='derfp') if not hasattr(self, 'fingerprintprimes'): calc = \ FingerprintPrimeCalculator(neighborlist=self.neighborlist, Gs=p.Gs, cutoff=p.cutoff, fortran=self.fortran) self.fingerprintprimes = \ Data(filename='%s-fingerprint-primes' % self.dblabel, calculator=calc) self.fingerprintprimes.calculate_items( images, parallel=parallel, log=log) log('...fingerprint derivatives calculated.', toc='derfp') # Calculators ################################################################# # Neighborlist Calculator class NeighborlistCalculator: """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- cutoff : float Radius above which neighbor interactions are ignored. """ def __init__(self, cutoff): self.globals = Parameters({'cutoff': cutoff}) self.keyed = Parameters() self.parallel_command = 'calculate_neighborlists' def calculate(self, image, key): """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- image : object ASE atoms object. key : str key of the image after being hashed. """ cutoff = self.globals.cutoff n = NeighborList(cutoffs=[cutoff / 2.] * len(image), self_interaction=False, bothways=True, skin=0.) n.update(image) return [n.get_neighbors(index) for index in range(len(image))] class FingerprintCalculator: """For integration with .utilities.Data Parameters ---------- neighborlist : list of str List of neighbors. Gs : dict Dictionary of symbols and lists of dictionaries for making symmetry functions. Either auto-genetrated, or given in the following form, for example: >>> Gs = {"O": [{"type":"G2", "element":"O", "eta":10.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":5., "gamma":1., "zeta":1.0}], ... "Au": [{"type":"G2", "element":"O", "eta":2.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":2., "gamma":1., "zeta":5.0}]} cutoff : float Radius above which neighbor interactions are ignored. fortran : bool If True, will use fortran modules, if False, will not. """ def __init__(self, neighborlist, Gs, cutoff, fortran): self.globals = Parameters({'cutoff': cutoff, 'Gs': Gs}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprints' self.fortran = fortran def calculate(self, image, key): """Makes a list of fingerprints, one per atom, for the fed image. Parameters ---------- image : object ASE atoms object. key : str key of the image after being hashed. """ self.atoms = image nl = self.keyed.neighborlist[key] fingerprints = [] for atom in image: symbol = atom.symbol index = atom.index neighborindices, neighboroffsets = nl[index] neighborsymbols = [image[_].symbol for _ in neighborindices] neighborpositions = \ [image.positions[neighbor] + np.dot(offset, image.cell) for (neighbor, offset) in zip(neighborindices, neighboroffsets)] indexfp = self.get_fingerprint( index, symbol, neighborsymbols, neighborpositions) fingerprints.append(indexfp) return fingerprints def get_fingerprint(self, index, symbol, neighborsymbols, neighborpositions): """Returns the fingerprint of symmetry function values for atom specified by its index and symbol. neighborsymbols and neighborpositions are lists of neighbors' symbols and Cartesian positions, respectively. Parameters ---------- index : int Index of the center atom. symbol : str Symbol of the center atom. neighborsymbols : list of str List of neighbors' symbols. neighborpositions : list of list of float List of Cartesian atomic positions. Returns ------- symbol, fingerprint : list of float fingerprints for atom specified by its index and symbol. """ Ri = self.atoms[index].position num_symmetries = len(self.globals.Gs[symbol]) fingerprint = [None] * num_symmetries for count in range(num_symmetries): G = self.globals.Gs[symbol][count] if G['type'] == 'G2': ridge = calculate_G2(neighborsymbols, neighborpositions, G['element'], G['eta'], self.globals.cutoff, Ri, self.fortran) elif G['type'] == 'G4': ridge = calculate_G4(neighborsymbols, neighborpositions, G['elements'], G['gamma'], G['zeta'], G['eta'], self.globals.cutoff, Ri, self.fortran) else: raise NotImplementedError('Unknown G type: %s' % G['type']) fingerprint[count] = ridge return symbol, fingerprint class FingerprintPrimeCalculator: """For integration with .utilities.Data Parameters ---------- neighborlist : list of str List of neighbors. Gs : dict Dictionary of symbols and lists of dictionaries for making symmetry functions. Either auto-genetrated, or given in the following form, for example: >>> Gs = {"O": [{"type":"G2", "element":"O", "eta":10.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":5., "gamma":1., "zeta":1.0}], ... "Au": [{"type":"G2", "element":"O", "eta":2.}, ... {"type":"G4", "elements":["O", "Au"], ... "eta":2., "gamma":1., "zeta":5.0}]} cutoff : float Radius above which neighbor interactions are ignored. fortran : bool If True, will use fortran modules, if False, will not. """ def __init__(self, neighborlist, Gs, cutoff, fortran): self.globals = Parameters({'cutoff': cutoff, 'Gs': Gs}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprint_primes' self.fortran = fortran def calculate(self, image, key): """Makes a list of fingerprint derivatives, one per atom, for the fed image. Parameters ---------- image : object ASE atoms object. key : str key of the image after being hashed. """ self.atoms = image nl = self.keyed.neighborlist[key] fingerprintprimes = {} for atom in image: selfsymbol = atom.symbol selfindex = atom.index selfneighborindices, selfneighboroffsets = nl[selfindex] selfneighborsymbols = [ image[_].symbol for _ in selfneighborindices] selfneighborpositions = [image.positions[_index] + np.dot(_offset, image.get_cell()) for _index, _offset in zip(selfneighborindices, selfneighboroffsets)] for i in range(3): # Calculating derivative of fingerprints of self atom w.r.t. # coordinates of itself. fpprime = self.get_fingerprintprime( selfindex, selfsymbol, selfneighborindices, selfneighborsymbols, selfneighborpositions, selfindex, i) fingerprintprimes[ (selfindex, selfsymbol, selfindex, selfsymbol, i)] = \ fpprime # Calculating derivative of fingerprints of neighbor atom # w.r.t. coordinates of self atom. for nindex, nsymbol, noffset in \ zip(selfneighborindices, selfneighborsymbols, selfneighboroffsets): # for calculating forces, summation runs over neighbor # atoms of type II (within the main cell only) if noffset.all() == 0: nneighborindices, nneighboroffsets = nl[nindex] nneighborsymbols = \ [image[_].symbol for _ in nneighborindices] neighborpositions = [image.positions[_index] + np.dot(_offset, image.get_cell()) for _index, _offset in zip(nneighborindices, nneighboroffsets)] # for calculating derivatives of fingerprints, # summation runs over neighboring atoms of type # I (either inside or outside the main cell) fpprime = self.get_fingerprintprime( nindex, nsymbol, nneighborindices, nneighborsymbols, neighborpositions, selfindex, i) fingerprintprimes[ (selfindex, selfsymbol, nindex, nsymbol, i)] = \ fpprime return fingerprintprimes def get_fingerprintprime(self, index, symbol, neighborindices, neighborsymbols, neighborpositions, m, l): """ Returns the value of the derivative of G for atom with index and symbol with respect to coordinate x_{l} of atom index m. neighborindices, neighborsymbols and neighborpositions are lists of neighbors' indices, symbols and Cartesian positions, respectively. Parameters ---------- index : int Index of the center atom. symbol : str Symbol of the center atom. neighborindices : list of int List of neighbors' indices. neighborsymbols : list of str List of neighbors' symbols. neighborpositions : list of list of float List of Cartesian atomic positions. m : int Index of the pair atom. l : int Direction of the derivative; is an integer from 0 to 2. Returns ------- fingerprintprime : list of float The value of the derivative of the fingerprints for atom with index and symbol with respect to coordinate x_{l} of atom index m. """ num_symmetries = len(self.globals.Gs[symbol]) Rindex = self.atoms.positions[index] fingerprintprime = [None] * num_symmetries for count in range(num_symmetries): G = self.globals.Gs[symbol][count] if G['type'] == 'G2': ridge = calculate_G2_prime( neighborindices, neighborsymbols, neighborpositions, G['element'], G['eta'], self.globals.cutoff, index, Rindex, m, l, self.fortran) elif G['type'] == 'G4': ridge = calculate_G4_prime( neighborindices, neighborsymbols, neighborpositions, G['elements'], G['gamma'], G['zeta'], G['eta'], self.globals.cutoff, index, Rindex, m, l, self.fortran) else: raise NotImplementedError('Unknown G type: %s' % G['type']) fingerprintprime[count] = ridge return fingerprintprime # Auxiliary functions ######################################################### def calculate_G2(neighborsymbols, neighborpositions, G_element, eta, cutoff, Ri, fortran): """Calculate G2 symmetry function. Ideally this will not be used but will be a template for how to build the fortran version (and serves as a slow backup if the fortran one goes uncompiled). See Eq. 13a of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- neighborsymbols : list of str List of symbols of all neighbor atoms. neighborpositions : list of list of floats List of Cartesian atomic positions. G_element : str Chemical symbol of the center atom. eta : float Parameter of Gaussian symmetry functions. cutoff : dict Cutoff function, typically from amp.descriptor.cutoffs. Should be also formatted as a dictionary by todict method, e.g. cutoff=Cosine(6.5).todict() Ri : list Position of the center atom. Should be fed as a list of three floats. fortran : bool If True, will use the fortran subroutines, else will not. Returns ------- ridge : float G2 fingerprint. """ if fortran: # fortran version; faster G_number = [atomic_numbers[G_element]] neighbornumbers = \ [atomic_numbers[symbol] for symbol in neighborsymbols] if len(neighbornumbers) == 0: ridge = 0. else: cutofffn = cutoff['name'] Rc = cutoff['kwargs']['Rc'] args_calculate_g2 = dict( neighbornumbers=neighbornumbers, neighborpositions=neighborpositions, g_number=G_number, g_eta=eta, rc=Rc, cutofffn=cutofffn, ri=Ri ) if cutofffn == 'Polynomial': args_calculate_g2['p_gamma'] = cutoff['kwargs']['gamma'] ridge = fmodules.calculate_g2(**args_calculate_g2) else: Rc = cutoff['kwargs']['Rc'] cutoff_fxn = dict2cutoff(cutoff) ridge = 0. # One aspect of a fingerprint :) num_neighbors = len(neighborpositions) # number of neighboring atoms for count in range(num_neighbors): symbol = neighborsymbols[count] Rj = neighborpositions[count] if symbol == G_element: Rij = np.linalg.norm(Rj - Ri) args_cutoff_fxn = dict(Rij=Rij) if cutoff['name'] == 'Polynomial': args_cutoff_fxn['gamma'] = cutoff['kwargs']['gamma'] ridge += (np.exp(-eta * (Rij ** 2.) / (Rc ** 2.)) * cutoff_fxn(**args_cutoff_fxn)) return ridge def calculate_G4(neighborsymbols, neighborpositions, G_elements, gamma, zeta, eta, cutoff, Ri, fortran): """Calculate G4 symmetry function. Ideally this will not be used but will be a template for how to build the fortran version (and serves as a slow backup if the fortran one goes uncompiled). See Eq. 13c of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- neighborsymbols : list of str List of symbols of neighboring atoms. neighborpositions : list of list of floats List of Cartesian atomic positions of neighboring atoms. G_elements : list of str A list of two members, each member is the chemical species of one of the neighboring atoms forming the triangle with the center atom. gamma : float Parameter of Gaussian symmetry functions. zeta : float Parameter of Gaussian symmetry functions. eta : float Parameter of Gaussian symmetry functions. cutoff : dict Cutoff function, typically from amp.descriptor.cutoffs. Should be also formatted as a dictionary by todict method, e.g. cutoff=Cosine(6.5).todict() Ri : list Position of the center atom. Should be fed as a list of three floats. fortran : bool If True, will use the fortran subroutines, else will not. Returns ------- ridge : float G4 fingerprint. """ if fortran: # fortran version; faster G_numbers = sorted([atomic_numbers[el] for el in G_elements]) neighbornumbers = \ [atomic_numbers[symbol] for symbol in neighborsymbols] if len(neighborpositions) == 0: return 0. else: cutofffn = cutoff['name'] Rc = cutoff['kwargs']['Rc'] args_calculate_g4 = dict( neighbornumbers=neighbornumbers, neighborpositions=neighborpositions, g_numbers=G_numbers, g_gamma=gamma, g_zeta=zeta, g_eta=eta, rc=Rc, cutofffn=cutofffn, ri=Ri ) if cutofffn == 'Polynomial': args_calculate_g4['p_gamma'] = cutoff['kwargs']['gamma'] ridge = fmodules.calculate_g4(**args_calculate_g4) return ridge else: Rc = cutoff['kwargs']['Rc'] cutoff_fxn = dict2cutoff(cutoff) ridge = 0. counts = range(len(neighborpositions)) for j in counts: for k in counts[(j + 1):]: els = sorted([neighborsymbols[j], neighborsymbols[k]]) if els != G_elements: continue Rij_vector = neighborpositions[j] - Ri Rij = np.linalg.norm(Rij_vector) Rik_vector = neighborpositions[k] - Ri Rik = np.linalg.norm(Rik_vector) Rjk_vector = neighborpositions[k] - neighborpositions[j] Rjk = np.linalg.norm(Rjk_vector) cos_theta_ijk = np.dot(Rij_vector, Rik_vector) / Rij / Rik term = (1. + gamma * cos_theta_ijk) ** zeta term *= np.exp(-eta * (Rij ** 2. + Rik ** 2. + Rjk ** 2.) / (Rc ** 2.)) _Rij = dict(Rij=Rij) _Rik = dict(Rij=Rik) _Rjk = dict(Rij=Rjk) if cutoff['name'] == 'Polynomial': _Rij['gamma'] = cutoff['kwargs']['gamma'] _Rik['gamma'] = cutoff['kwargs']['gamma'] _Rjk['gamma'] = cutoff['kwargs']['gamma'] term *= cutoff_fxn(**_Rij) term *= cutoff_fxn(**_Rik) term *= cutoff_fxn(**_Rjk) ridge += term ridge *= 2. ** (1. - zeta) return ridge def make_symmetry_functions(elements, type, etas, zetas=None, gammas=None): """Helper function to create Gaussian symmetry functions. Returns a list of dictionaries with symmetry function parameters in the format expected by the Gaussian class. Parameters ---------- elements : list of str List of element types. The first in the list is considered the central element for this fingerprint. #FIXME: Does that matter? type : str Either G2 or G4. etas : list of floats eta values to use in G2 or G4 fingerprints zetas : list of floats zeta values to use in G4 fingerprints gammas : list of floats gamma values to use in G4 fingerprints Returns ------- G : list of dicts A list, each item in the list contains a dictionary of fingerprint parameters. """ if type == 'G2': G = [{'type': 'G2', 'element': element, 'eta': eta} for eta in etas for element in elements] return G elif type == 'G4': G = [] for eta in etas: for zeta in zetas: for gamma in gammas: for i1, el1 in enumerate(elements): for el2 in elements[i1:]: els = sorted([el1, el2]) G.append({'type': 'G4', 'elements': els, 'eta': eta, 'gamma': gamma, 'zeta': zeta}) return G raise NotImplementedError('Unknown type: {}.'.format(type)) def make_default_symmetry_functions(elements): """Makes symmetry functions as in Nano Letters 14:2670, 2014. Parameters ---------- elements : list of str List of the elements, as in: ["C", "O", "H", "Cu"]. Returns ------- G : dict of lists The generated symmetry function parameters. """ G = {} for element0 in elements: # Radial symmetry functions. etas = [0.05, 4., 20., 80.] _G = [{'type': 'G2', 'element': element, 'eta': eta} for eta in etas for element in elements] # Angular symmetry functions. etas = [0.005] zetas = [1., 4.] gammas = [+1., -1.] for eta in etas: for zeta in zetas: for gamma in gammas: for i1, el1 in enumerate(elements): for el2 in elements[i1:]: els = sorted([el1, el2]) _G.append({'type': 'G4', 'elements': els, 'eta': eta, 'gamma': gamma, 'zeta': zeta}) G[element0] = _G return G def Kronecker(i, j): """Kronecker delta function. Parameters ---------- i : int First index of Kronecker delta. j : int Second index of Kronecker delta. Returns ------- int The value of the Kronecker delta. """ if i == j: return 1 else: return 0 def dRij_dRml_vector(i, j, m, l): """Returns the derivative of the position vector R_{ij} with respect to x_{l} of itomic index m. See Eq. 14d of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- i : int Index of the first atom. j : int Index of the second atom. m : int Index of the atom force is acting on. l : int Direction of force. Returns ------- list of float The derivative of the position vector R_{ij} with respect to x_{l} of atomic index m. """ if (m != i) and (m != j): return [0, 0, 0] else: dRij_dRml_vector = [None, None, None] c1 = Kronecker(m, j) - Kronecker(m, i) dRij_dRml_vector[0] = c1 * Kronecker(0, l) dRij_dRml_vector[1] = c1 * Kronecker(1, l) dRij_dRml_vector[2] = c1 * Kronecker(2, l) return dRij_dRml_vector def dRij_dRml(i, j, Ri, Rj, m, l): """Returns the derivative of the norm of position vector R_{ij} with respect to coordinate x_{l} of atomic index m. See Eq. 14c of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- i : int Index of the first atom. j : int Index of the second atom. Ri : float Position of the first atom. Rj : float Position of the second atom. m : int Index of the atom force is acting on. l : int Direction of force. Returns ------- dRij_dRml : list of float The derivative of the noRi of position vector R_{ij} with respect to x_{l} of atomic index m. """ Rij = np.linalg.norm(Rj - Ri) if m == i and i != j: # i != j is necessary for periodic systems dRij_dRml = -(Rj[l] - Ri[l]) / Rij elif m == j and i != j: # i != j is necessary for periodic systems dRij_dRml = (Rj[l] - Ri[l]) / Rij else: dRij_dRml = 0 return dRij_dRml def dCos_theta_ijk_dR_ml(i, j, k, Ri, Rj, Rk, m, l): """Returns the derivative of Cos(theta_{ijk}) with respect to x_{l} of atomic index m. See Eq. 14f of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- i : int Index of the center atom. j : int Index of the first atom. k : int Index of the second atom. Ri : float Position of the center atom. Rj : float Position of the first atom. Rk : float Position of the second atom. m : int Index of the atom force is acting on. l : int Direction of force. Returns ------- dCos_theta_ijk_dR_ml : float Derivative of Cos(theta_{ijk}) with respect to x_{l} of atomic index m. """ Rij_vector = Rj - Ri Rij = np.linalg.norm(Rij_vector) Rik_vector = Rk - Ri Rik = np.linalg.norm(Rik_vector) dCos_theta_ijk_dR_ml = 0 dRijdRml = dRij_dRml_vector(i, j, m, l) if np.array(dRijdRml).any() != 0: dCos_theta_ijk_dR_ml += np.dot(dRijdRml, Rik_vector) / (Rij * Rik) dRikdRml = dRij_dRml_vector(i, k, m, l) if np.array(dRikdRml).any() != 0: dCos_theta_ijk_dR_ml += np.dot(Rij_vector, dRikdRml) / (Rij * Rik) dRijdRml = dRij_dRml(i, j, Ri, Rj, m, l) if dRijdRml != 0: dCos_theta_ijk_dR_ml += - np.dot(Rij_vector, Rik_vector) * dRijdRml / \ ((Rij ** 2.) * Rik) dRikdRml = dRij_dRml(i, k, Ri, Rk, m, l) if dRikdRml != 0: dCos_theta_ijk_dR_ml += - np.dot(Rij_vector, Rik_vector) * dRikdRml / \ (Rij * (Rik ** 2.)) return dCos_theta_ijk_dR_ml def calculate_G2_prime(neighborindices, neighborsymbols, neighborpositions, G_element, eta, cutoff, i, Ri, m, l, fortran): """Calculates coordinate derivative of G2 symmetry function for atom at index i and position Ri with respect to coordinate x_{l} of atom index m. See Eq. 13b of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters --------- neighborindices : list of int List of int of neighboring atoms. neighborsymbols : list of str List of symbols of neighboring atoms. neighborpositions : list of list of float List of Cartesian atomic positions of neighboring atoms. G_element : dict Symmetry functions of the center atom. eta : float Parameter of Behler symmetry functions. cutoff : dict Cutoff function, typically from amp.descriptor.cutoffs. Should be also formatted as a dictionary by todict method, e.g. cutoff=Cosine(6.5).todict() i : int Index of the center atom. Ri : list Position of the center atom. Should be fed as a list of three floats. m : int Index of the atom force is acting on. l : int Direction of force. fortran : bool If True, will use the fortran subroutines, else will not. Returns ------- ridge : float Coordinate derivative of G2 symmetry function for atom at index a and position Ri with respect to coordinate x_{l} of atom index m. """ if fortran: # fortran version; faster G_number = [atomic_numbers[G_element]] neighbornumbers = \ [atomic_numbers[symbol] for symbol in neighborsymbols] if len(neighborpositions) == 0: ridge = 0. else: cutofffn = cutoff['name'] Rc = cutoff['kwargs']['Rc'] args_calculate_g2_prime = dict( neighborindices=list(neighborindices), neighbornumbers=neighbornumbers, neighborpositions=neighborpositions, g_number=G_number, g_eta=eta, rc=Rc, cutofffn=cutofffn, i=i, ri=Ri, m=m, l=l ) if cutofffn == 'Polynomial': args_calculate_g2_prime['p_gamma'] = cutoff['kwargs']['gamma'] ridge = fmodules.calculate_g2_prime(**args_calculate_g2_prime) else: Rc = cutoff['kwargs']['Rc'] cutoff_fxn = dict2cutoff(cutoff) ridge = 0. # One aspect of a fingerprint :) num_neighbors = len(neighborpositions) # number of neighboring atoms for count in range(num_neighbors): symbol = neighborsymbols[count] Rj = neighborpositions[count] j = neighborindices[count] if symbol == G_element: dRijdRml = dRij_dRml(i, j, Ri, Rj, m, l) if dRijdRml != 0: Rij = np.linalg.norm(Rj - Ri) args_cutoff_fxn = dict(Rij=Rij) if cutoff['name'] == 'Polynomial': args_cutoff_fxn['gamma'] = cutoff['kwargs']['gamma'] term1 = (-2. * eta * Rij * cutoff_fxn(**args_cutoff_fxn) / (Rc ** 2.) + cutoff_fxn.prime(**args_cutoff_fxn)) ridge += np.exp(- eta * (Rij ** 2.) / (Rc ** 2.)) * \ term1 * dRijdRml return ridge def calculate_G4_prime(neighborindices, neighborsymbols, neighborpositions, G_elements, gamma, zeta, eta, cutoff, i, Ri, m, l, fortran): """Calculates coordinate derivative of G4 symmetry function for atom at index i and position Ri with respect to coordinate x_{l} of atom index m. See Eq. 13d of the supplementary information of Khorshidi, Peterson, CPC(2016). Parameters ---------- neighborindices : list of int List of int of neighboring atoms. neighborsymbols : list of str List of symbols of neighboring atoms. neighborpositions : list of list of float List of Cartesian atomic positions of neighboring atoms. G_elements : list of str A list of two members, each member is the chemical species of one of the neighboring atoms forming the triangle with the center atom. gamma : float Parameter of Behler symmetry functions. zeta : float Parameter of Behler symmetry functions. eta : float Parameter of Behler symmetry functions. cutoff : dict Cutoff function, typically from amp.descriptor.cutoffs. Should be also formatted as a dictionary by todict method, e.g. cutoff=Cosine(6.5).todict() i : int Index of the center atom. Ri : list Position of the center atom. Should be fed as a list of three floats. m : int Index of the atom force is acting on. l : int Direction of force. fortran : bool If True, will use the fortran subroutines, else will not. Returns ------- ridge : float Coordinate derivative of G4 symmetry function for atom at index i and position Ri with respect to coordinate x_{l} of atom index m. """ if fortran: # fortran version; faster G_numbers = sorted([atomic_numbers[el] for el in G_elements]) neighbornumbers = [atomic_numbers[symbol] for symbol in neighborsymbols] if len(neighborpositions) == 0: ridge = 0. else: cutofffn = cutoff['name'] Rc = cutoff['kwargs']['Rc'] args_calculate_g4_prime = dict( neighborindices=list(neighborindices), neighbornumbers=neighbornumbers, neighborpositions=neighborpositions, g_numbers=G_numbers, g_gamma=gamma, g_zeta=zeta, g_eta=eta, rc=Rc, cutofffn=cutofffn, i=i, ri=Ri, m=m, l=l ) if cutofffn == 'Polynomial': args_calculate_g4_prime['p_gamma'] = cutoff['kwargs']['gamma'] ridge = fmodules.calculate_g4_prime(**args_calculate_g4_prime) else: Rc = cutoff['kwargs']['Rc'] cutoff_fxn = dict2cutoff(cutoff) ridge = 0. # number of neighboring atoms counts = range(len(neighborpositions)) for j in counts: for k in counts[(j + 1):]: els = sorted([neighborsymbols[j], neighborsymbols[k]]) if els != G_elements: continue Rj = neighborpositions[j] Rk = neighborpositions[k] Rij_vector = neighborpositions[j] - Ri Rij = np.linalg.norm(Rij_vector) Rik_vector = neighborpositions[k] - Ri Rik = np.linalg.norm(Rik_vector) Rjk_vector = neighborpositions[k] - neighborpositions[j] Rjk = np.linalg.norm(Rjk_vector) cos_theta_ijk = np.dot(Rij_vector, Rik_vector) / Rij / Rik c1 = (1. + gamma * cos_theta_ijk) _Rij = dict(Rij=Rij) _Rik = dict(Rij=Rik) _Rjk = dict(Rij=Rjk) if cutoff['name'] == 'Polynomial': _Rij['gamma'] = cutoff['kwargs']['gamma'] _Rik['gamma'] = cutoff['kwargs']['gamma'] _Rjk['gamma'] = cutoff['kwargs']['gamma'] fcRij = cutoff_fxn(**_Rij) fcRik = cutoff_fxn(**_Rik) fcRjk = cutoff_fxn(**_Rjk) if zeta == 1: term1 = \ np.exp(- eta * (Rij ** 2. + Rik ** 2. + Rjk ** 2.) / (Rc ** 2.)) else: term1 = c1 ** (zeta - 1.) * \ np.exp(- eta * (Rij ** 2. + Rik ** 2. + Rjk ** 2.) / (Rc ** 2.)) term2 = 0. fcRijfcRikfcRjk = fcRij * fcRik * fcRjk dCosthetadRml = dCos_theta_ijk_dR_ml(i, neighborindices[j], neighborindices[k], Ri, Rj, Rk, m, l) if dCosthetadRml != 0: term2 += gamma * zeta * dCosthetadRml dRijdRml = dRij_dRml(i, neighborindices[j], Ri, Rj, m, l) if dRijdRml != 0: term2 += -2. * c1 * eta * Rij * dRijdRml / (Rc ** 2.) dRikdRml = dRij_dRml(i, neighborindices[k], Ri, Rk, m, l) if dRikdRml != 0: term2 += -2. * c1 * eta * Rik * dRikdRml / (Rc ** 2.) dRjkdRml = dRij_dRml(neighborindices[j], neighborindices[k], Rj, Rk, m, l) if dRjkdRml != 0: term2 += -2. * c1 * eta * Rjk * dRjkdRml / (Rc ** 2.) term3 = fcRijfcRikfcRjk * term2 term4 = cutoff_fxn.prime(**_Rij) * dRijdRml * fcRik * fcRjk term5 = fcRij * cutoff_fxn.prime(**_Rik) * dRikdRml * fcRjk term6 = fcRij * fcRik * cutoff_fxn.prime(**_Rjk) * dRjkdRml ridge += term1 * (term3 + c1 * (term4 + term5 + term6)) ridge *= 2. ** (1. - zeta) return ridge if __name__ == "__main__": """Directly calling this module; apparently from another node. Calls should come as python -m amp.descriptor.gaussian id hostname:port This session will then start a zmq session with that socket, labeling itself with id. Instructions on what to do will come from the socket. """ import sys import tempfile import zmq from ..utilities import MessageDictionary fortran = False if fmodules is None else True hostsocket = sys.argv[-1] proc_id = sys.argv[-2] msg = MessageDictionary(proc_id) # Send standard lines to stdout signaling process started and where # error is directed. This should be caught by pxssh. (This could # alternatively be done by zmq, but this works.) print('') # Signal that program started. sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('Log and error written to %s' % sys.stderr.name) sys.stderr.write('initiated\n') sys.stderr.flush() # Establish client session via zmq; find purpose. context = zmq.Context() sys.stderr.write('context started\n') sys.stderr.flush() socket = context.socket(zmq.REQ) sys.stderr.write('socket started\n') sys.stderr.flush() socket.connect('tcp://%s' % hostsocket) sys.stderr.write('connection made\n') sys.stderr.flush() socket.send_pyobj(msg('')) sys.stderr.write('message sent\n') sys.stderr.flush() purpose = socket.recv_pyobj() sys.stderr.write('purpose received\n') sys.stderr.flush() sys.stderr.write('purpose: %s \n' % purpose) sys.stderr.flush() if purpose == 'calculate_neighborlists': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() # sys.stderr.write(str(images)) # Just to see if they are there. # Perform the calculations. calc = NeighborlistCalculator(cutoff=cutoff) neighborlist = {} # for key in images.iterkeys(): while len(images) > 0: key, image = images.popitem() # Reduce memory. neighborlist[key] = calc.calculate(image, key) # Send the results. socket.send_pyobj(msg('', neighborlist)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprints': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'Gs')) Gs = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() calc = FingerprintCalculator(neighborlist, Gs, cutoff, fortran) result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprint_primes': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'Gs')) Gs = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() calc = FingerprintPrimeCalculator(neighborlist, Gs, cutoff, fortran) result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. else: raise NotImplementedError('purpose %s unknown.' % purpose) andrewpeterson-amp-4878fc892f2c/amp/descriptor/zernike.f90000066400000000000000000000315321332417112400233740ustar00rootroot00000000000000!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine calculate_zernike_prime(n, l, n_length, n_indices, & numbers, rs, g_numbers, cutoff, indexx, home, p, q, & fac_length, factorial, norm_prime, cutofffn, p_gamma) use cutoffs implicit none integer:: n, l integer:: indexx, p, q, n_length, fac_length integer, dimension(n_length):: n_indices, numbers, g_numbers double precision, dimension(n_length, 3):: rs double precision, dimension(3):: home double precision, dimension(fac_length):: factorial double precision:: cutoff ! gamma parameter for the polynomial cutoff double precision, optional:: p_gamma character(len=20):: cutofffn complex*16:: norm_prime !f2py intent(in):: n, l, n_indices, numbers, g_numbers, rs, p_gamma !f2py intent(in):: home, indexx, p, q, cutoff, n_length, fac_length !f2py intent(out):: norm_prime integer:: m complex*16:: c_nlm, c_nlm_prime, z_nlm_, z_nlm, & z_nlm_prime, z_nlm_prime_ integer:: n_index, n_symbol, iter double precision, dimension(3):: neighbor double precision:: x, y, z, rho norm_prime = (0.0d0, 0.0d0) do m = 0, l c_nlm = (0.0d0, 0.0d0) c_nlm_prime = (0.0d0, 0.0d0) do iter = 1, n_length n_index = n_indices(iter) n_symbol = numbers(iter) neighbor(1) = rs(iter, 1) neighbor(2) = rs(iter, 2) neighbor(3) = rs(iter, 3) x = (neighbor(1) - home(1)) / cutoff y = (neighbor(2) - home(2)) / cutoff z = (neighbor(3) - home(3)) / cutoff rho = (x ** 2.0d0 + y ** 2.0d0 + z ** 2.0d0) ** 0.5d0 call calculate_z(n, l, m, x, y, z, factorial, & fac_length, z_nlm_) ! Calculate z_nlm if (present(p_gamma)) then z_nlm = z_nlm_ * cutoff_fxn(rho * cutoff, & cutoff, cutofffn, p_gamma) ! Calculates z_nlm_prime z_nlm_prime = z_nlm_ * & cutoff_fxn_prime(rho * cutoff, cutoff, & cutofffn, p_gamma) * & der_position(indexx, n_index, home, neighbor, p, q) else z_nlm = z_nlm_ * cutoff_fxn(rho * cutoff, & cutoff, cutofffn) ! Calculates z_nlm_prime z_nlm_prime = z_nlm_ * & cutoff_fxn_prime(rho * cutoff, cutoff, & cutofffn) * & der_position(indexx, n_index, home, neighbor, p, q) endif call calculate_z_prime(n, l, m, x, y, z, q, factorial, & fac_length, z_nlm_prime_) if (kronecker(n_index, p) - & kronecker(indexx, p) == 1) then if (present(p_gamma)) then z_nlm_prime = z_nlm_prime + & cutoff_fxn(rho * cutoff, cutoff, & cutofffn, p_gamma) * z_nlm_prime_ / & cutoff else z_nlm_prime = z_nlm_prime + & cutoff_fxn(rho * cutoff, cutoff, & cutofffn) * z_nlm_prime_ / cutoff end if else if (kronecker(n_index, p) - kronecker(indexx, p) & == -1) then if (present(p_gamma)) then z_nlm_prime = z_nlm_prime - & cutoff_fxn(rho * cutoff, cutoff, & cutofffn, p_gamma) * z_nlm_prime_ / & cutoff else z_nlm_prime = z_nlm_prime - & cutoff_fxn(rho * cutoff, cutoff, & cutofffn) * z_nlm_prime_ / cutoff end if end if ! sum over neighbors c_nlm = c_nlm + g_numbers(iter) * conjg(z_nlm) c_nlm_prime = c_nlm_prime + & g_numbers(iter) * conjg(z_nlm_prime) end do ! sum over m values if (m == 0) then norm_prime = norm_prime + & 2.0d0 * c_nlm * conjg(c_nlm_prime) else norm_prime = norm_prime + & 4.0d0 * c_nlm * conjg(c_nlm_prime) end if enddo CONTAINS function der_position(mm, nn, Rm, Rn, ll, ii) implicit none integer:: mm, nn, ll, ii, xyz double precision, dimension(3):: Rm, Rn, Rmn_ double precision:: der_position, Rmn do xyz = 1, 3 Rmn_(xyz) = Rm(xyz) - Rn(xyz) end do Rmn = sqrt(dot_product(Rmn_, Rmn_)) if ((ll == mm) .AND. (mm /= nn)) then der_position = (Rm(ii + 1) - Rn(ii + 1)) / Rmn else if ((ll == nn) .AND. (mm /= nn)) then der_position = - (Rm(ii + 1) - Rn(ii + 1)) / Rmn else der_position = 0.0d0 end if end function function kronecker(i, j) implicit none integer:: i, j integer:: kronecker if (i == j) then kronecker = 1 else kronecker = 0 end if end function end subroutine calculate_zernike_prime subroutine calculate_z(n, l, m, x, y, z, factorial, length, & output) implicit none integer:: n, l, m, length double precision:: x, y, z double precision, dimension(length):: factorial complex*16:: output, ii, term4, term6 !f2py intent(in):: n, l, m, x, y, z, factorial, length !f2py intent(out):: output integer:: k, nu, alpha, beta, eta, u, mu, r, s, t double precision:: term1, term2, q, b1, b2, term3 double precision:: term5, b5, b6, b7, b8, pi pi = 4.0d0 * datan(1.0d0) output = (0.0d0, 0.0d0) term1 = sqrt((2.0d0 * l + 1.0d0) * & factorial(int(2 * (l + m)) + 1) * & factorial(int(2 * (l - m)) + 1)) / factorial(int(2 * l) + 1) term2 = 2.0d0 ** (-m) ii = (0.0d0, 1.0d0) k = int((n - l) / 2.0d0) do nu = 0, k call calculate_q(nu, k, l, factorial, length, q) do alpha = 0, nu call binomial(float(nu), float(alpha), & factorial, length, b1) do beta = 0, nu - alpha call binomial(float(nu - alpha), float(beta), & factorial, length, b2) term3 = q * b1 * b2 do u = 0, m call binomial(float(m), float(u), factorial, & length, b5) term4 = ((-1.0d0)**(m - u)) * b5 * (ii**u) do mu = 0, int((l - m) / 2.0d0) call binomial(float(l), float(mu), & factorial, length, b6) call binomial(float(l - mu), float(m + mu),& factorial, length, b7) term5 = ((-1.0d0) ** mu) * (2.0d0 ** & (-2.0d0 * mu)) * b6 * b7 do eta = 0, mu call binomial(float(mu), float(eta), & factorial, length, b8) r = 2 * (eta + alpha) + u s = 2 * (mu - eta + beta) + m - u t = 2 * (nu - alpha - beta - mu) + l - m output = output + term3 * term4 & * term5 * b8 * (x ** r) & * (y ** s) * (z ** t) end do end do end do end do end do end do term6 = (ii) ** m output = term1 * term2 * term6 * output output = output / sqrt(4.0d0 * pi / 3.0d0) end subroutine calculate_z subroutine calculate_z_prime(n, l, m, x, y, z, p, factorial, & length, output) implicit none integer:: n, l, m, length, p double precision:: x, y, z double precision, dimension(length):: factorial complex*16:: output, ii, coefficient, term4, term6 !f2py intent(in):: n, l, m, x, y, z, factorial, p, length !f2py intent(out):: output integer:: k, nu, alpha, beta, eta, u, mu, r, s, t double precision:: term1, term2, q, b1, b2, term3 double precision:: term5, b3, b4, b5, b6, pi pi = 4.0d0 * datan(1.0d0) output = (0.0d0, 0.0d0) term1 = sqrt((2.0d0 * l + 1.0d0) * & factorial(int(2 * (l + m)) + 1) * & factorial(int(2 * (l - m)) + 1)) / & factorial(int(2 * l) + 1) term2 = 2.0d0 ** (-m) ii = (0.0d0, 1.0d0) k = int((n - l) / 2.) do nu = 0, k call calculate_q(nu, k, l, factorial, length, q) do alpha = 0, nu call binomial(float(nu), float(alpha), factorial, & length, b1) do beta = 0, nu - alpha call binomial(float(nu - alpha), float(beta), & factorial, length, b2) term3 = q * b1 * b2 do u = 0, m call binomial(float(m), float(u), factorial, length, & b3) term4 = ((-1.0d0)**(m - u)) * b3 * (ii**u) do mu = 0, int((l - m) / 2.) call binomial(float(l), float(mu), factorial, & length, b4) call binomial(float(l - mu), float(m + mu), & factorial, length, b5) term5 = & ((-1.0d0)**mu) * (2.0d0**(-2.0d0 * mu)) * b4 * b5 do eta = 0, mu call binomial(float(mu), float(eta), factorial, & length, b6) r = 2 * (eta + alpha) + u s = 2 * (mu - eta + beta) + m - u t = 2 * (nu - alpha - beta - mu) + l - m coefficient = term3 * term4 * term5 * b6 if (p == 0) then if (r .NE. 0) then output = output + coefficient * r * & (x ** (r - 1)) * (y ** s) * (z ** t) end if else if (p == 1) then if (s .NE. 0) then output = output + coefficient * s * & (x ** r) * (y ** (s - 1)) * (z ** t) end if else if (p == 2) then if (t .NE. 0) then output = output + coefficient * t * & (x ** r) * (y ** s) * (z ** (t - 1)) end if end if end do end do end do end do end do end do term6 = (ii) ** m output = term1 * term2 * term6 * output output = output / sqrt(4.0d0 * pi / 3.0d0) end subroutine calculate_z_prime subroutine calculate_q(nu, k, l, factorial, length, output) implicit none integer:: nu, k, l, length double precision, dimension(length):: factorial double precision:: output, b1, b2, b3, b4 !f2py intent(in):: nu, k, l, factorial !f2py intent(out):: output call binomial(float(k), float(nu), factorial, length, b1) call binomial(float(2 * k), float(k), factorial, length, b2) call binomial(float(2 * (k + l + nu) + 1), float(2 * k), & factorial, length, b3) call binomial(float(k + l + nu), float(k), factorial, & length, b4) output = ((-1.0d0) ** (k + nu)) * & sqrt((2.0d0 * l + 4.0d0 * k + 3.0d0) / 3.0d0) * b1 * b2 * & b3 / b4 / (2.0d0 ** (2.0d0 * k)) end subroutine calculate_q subroutine binomial(n, k, factorial, length, output) implicit none real(4):: n, k integer:: length double precision, dimension(length):: factorial double precision:: output !f2py intent(in):: n, k, factorial, length !f2py intent(out):: output output = factorial(INT(2 * n) + 1) / & factorial(INT(2 * k) + 1) / & factorial(INT(2 * (n - k)) + 1) end subroutine binomial !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! andrewpeterson-amp-4878fc892f2c/amp/descriptor/zernike.py000066400000000000000000001160451332417112400234310ustar00rootroot00000000000000import numpy as np from numpy import sqrt from ase.data import atomic_numbers from ase.calculators.calculator import Parameters from scipy.special import sph_harm from ..utilities import Data, Logger, importer from .cutoffs import Cosine, Polynomial, dict2cutoff NeighborList = importer('NeighborList') try: from .. import fmodules except ImportError: fmodules = None class Zernike(object): """Class that calculates Zernike fingerprints. Parameters ---------- cutoff : object or float Cutoff function, typically from amp.descriptor.cutoffs. Can be also fed as a float representing the radius above which neighbor interactions are ignored; in this case a cosine cutoff function will be employed. Default is a 6.5-Angstrom cosine cutoff. Gs : dict Dictionary of symbols and dictionaries for making symmetry functions. Either auto-genetrated, or given in the following form, for example: >>> Gs = {"Au": {"Au": 3., "O": 2.}, "O": {"Au": 5., "O": 10.}} This is basically the same as \eta in Eq. (16) of https://doi.org/10.1016/j.cpc.2016.05.010. nmax : integer or dict Maximum degree of Zernike polynomials that will be included in the fingerprint vector. Can be different values for different species fed as a dictionary with chemical elements as keys. dblabel : str Optional separate prefix/location for database files, including fingerprints, fingerprint derivatives, and neighborlists. This file location can be shared between calculator instances to avoid re-calculating redundant information. If not supplied, just uses the value from label. elements : list List of allowed elements present in the system. If not provided, will be found automatically. version : str Version of fingerprints. mode : str Can be either 'atom-centered' or 'image-centered'. fortran : bool If True, will use fortran modules, if False, will not. Raises ------ RuntimeError, TypeError """ def __init__(self, cutoff=Cosine(6.5), Gs=None, nmax=5, dblabel=None, elements=None, version='2016.02', mode='atom-centered', fortran=True): # Check of the version of descriptor, particularly if restarting. compatibleversions = ['2016.02', ] if (version is not None) and version not in compatibleversions: raise RuntimeError('Error: Trying to use Zernike fingerprints' ' version %s, but this module only supports' ' versions %s. You may need an older or ' ' newer version of Amp.' % (version, compatibleversions)) else: version = compatibleversions[-1] # Check that the mode is atom-centered. if mode != 'atom-centered': raise RuntimeError('Zernike scheme only works ' 'in atom-centered mode. %s ' 'specified.' % mode) # If the cutoff is provided as a number, Cosine function will be used # by default. if isinstance(cutoff, int) or isinstance(cutoff, float): cutoff = Cosine(cutoff) # If the cutoff is provided as a dictionary, assume we need to load it # with dict2cutoff. if type(cutoff) is dict: cutoff = dict2cutoff(cutoff) # The parameters dictionary contains the minimum information # to produce a compatible descriptor; that is, one that gives # an identical fingerprint when fed an ASE image. p = self.parameters = Parameters( {'importname': '.descriptor.zernike.Zernike', 'mode': 'atom-centered'}) p.version = version p.cutoff = cutoff.todict() if p.cutoff['name'] == 'Polynomial': self.gamma = cutoff.gamma p.Gs = Gs p.nmax = nmax p.elements = elements self.dblabel = dblabel self.fortran = fortran self.parent = None # Can hold a reference to main Amp instance. def tostring(self): """Returns an evaluatable representation of the calculator that can be used to restart the calculator.""" return self.parameters.tostring() def calculate_fingerprints(self, images, parallel=None, log=None, calculate_derivatives=False): """Calculates the fingerpints of the images, for the ones not already done. Parameters ---------- images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. parallel : dict Configuration for parallelization. Should be in same form as in amp.Amp. log : Logger object Write function at which to log data. Note this must be a callable function. calculate_derivatives : bool Decides whether or not fingerprintprimes should also be calculated. """ if parallel is None: parallel = {'cores': 1} log = Logger(file=None) if log is None else log if (self.dblabel is None) and hasattr(self.parent, 'dblabel'): self.dblabel = self.parent.dblabel self.dblabel = 'amp-data' if self.dblabel is None else self.dblabel p = self.parameters if p.cutoff['name'] == 'Cosine': log('Cutoff radius: %.2f ' % p.cutoff['kwargs']['Rc']) else: log('Cutoff radius: %.2f and gamma=%i ' % (p.cutoff['kwargs']['Rc'], self.gamma)) log('Cutoff function: %s' % repr(dict2cutoff(p.cutoff))) if p.elements is None: log('Finding unique set of elements in training data.') p.elements = set([atom.symbol for atoms in images.values() for atom in atoms]) p.elements = sorted(p.elements) log('%i unique elements included: ' % len(p.elements) + ', '.join(p.elements)) log('Maximum degree of Zernike polynomials:') if isinstance(p.nmax, dict): for _ in p.nmax.keys(): log(' %2s: %d' % (_, p.nmax[_])) else: log('nmax: %d' % p.nmax) if p.Gs is None: log('No coefficient for atomic density function supplied; ' 'creating defaults.') p.Gs = generate_coefficients(p.elements) log('Coefficients of atomic density function for each element:') for _ in p.Gs.keys(): log(' %2s: %s' % (_, str(p.Gs[_]))) # Counts the number of descriptors for each element. no_of_descriptors = {} for element in p.elements: count = 0 if isinstance(p.nmax, dict): for n in range(p.nmax[element] + 1): for l in range(n + 1): if (n - l) % 2 == 0: count += 1 else: for n in range(p.nmax + 1): for l in range(n + 1): if (n - l) % 2 == 0: count += 1 no_of_descriptors[element] = count log('Number of descriptors for each element:') for element in p.elements: log(' %2s: %d' % (element, no_of_descriptors.pop(element))) log('Calculating neighborlists...', tic='nl') if not hasattr(self, 'neighborlist'): calc = NeighborlistCalculator(cutoff=p.cutoff['kwargs']['Rc']) self.neighborlist = Data(filename='%s-neighborlists' % self.dblabel, calculator=calc) self.neighborlist.calculate_items(images, parallel=parallel, log=log) log('...neighborlists calculated.', toc='nl') log('Fingerprinting images...', tic='fp') if not hasattr(self, 'fingerprints'): calc = FingerprintCalculator(neighborlist=self.neighborlist, Gs=p.Gs, nmax=p.nmax, cutoff=p.cutoff, fortran=self.fortran) self.fingerprints = Data(filename='%s-fingerprints' % self.dblabel, calculator=calc) self.fingerprints.calculate_items(images, parallel=parallel, log=log) log('...fingerprints calculated.', toc='fp') if calculate_derivatives: log('Calculating fingerprint derivatives of images...', tic='derfp') if not hasattr(self, 'fingerprintprimes'): calc = \ FingerprintPrimeCalculator(neighborlist=self.neighborlist, Gs=p.Gs, nmax=p.nmax, cutoff=p.cutoff, fortran=self.fortran) self.fingerprintprimes = \ Data(filename='%s-fingerprint-primes' % self.dblabel, calculator=calc) self.fingerprintprimes.calculate_items( images, parallel=parallel, log=log) log('...fingerprint derivatives calculated.', toc='derfp') # Calculators ################################################################# # Neighborlist Calculator class NeighborlistCalculator: """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- cutoff : object or float Cutoff function, typically from amp.descriptor.cutoffs. Can be also fed as a float representing the radius above which neighbor interactions are ignored; in this case a cosine cutoff function will be employed. Default is a 6.5-Angstrom cosine cutoff. """ def __init__(self, cutoff): self.globals = Parameters({'cutoff': cutoff}) self.keyed = Parameters() self.parallel_command = 'calculate_neighborlists' def calculate(self, image, key): """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. Parameters ---------- image : object ASE atoms object. key : str Key of the image after being hashed. """ cutoff = self.globals.cutoff n = NeighborList(cutoffs=[cutoff / 2.] * len(image), self_interaction=False, bothways=True, skin=0.) n.update(image) return [n.get_neighbors(index) for index in range(len(image))] class FingerprintCalculator: """For integration with .utilities.Data""" def __init__(self, neighborlist, Gs, nmax, cutoff, fortran): self.globals = Parameters({'cutoff': cutoff, 'Gs': Gs, 'nmax': nmax}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprints' self.fortran = fortran self.cutoff = cutoff try: # for scipy v <= 0.90 from scipy import factorial as fac except ImportError: try: # for scipy v >= 0.10 from scipy.misc import factorial as fac except ImportError: # for newer version of scipy from scipy.special import factorial as fac self.factorial = [fac(0.5 * _) for _ in range(4 * nmax + 3)] def calculate(self, image, key): """Makes a list of fingerprints, one per atom, for the fed image. Parameters ---------- image : object ASE atoms object. key : str Key of the image after being hashed. """ nl = self.keyed.neighborlist[key] fingerprints = [] for atom in image: symbol = atom.symbol index = atom.index neighbors, offsets = nl[index] neighborsymbols = [image[_].symbol for _ in neighbors] Rs = [image.positions[neighbor] + np.dot(offset, image.cell) for (neighbor, offset) in zip(neighbors, offsets)] self.atoms = image indexfp = self.get_fingerprint(index, symbol, neighborsymbols, Rs) fingerprints.append(indexfp) return fingerprints def get_fingerprint(self, index, symbol, n_symbols, Rs): """Returns the fingerprint of symmetry function values for atom specified by its index and symbol. n_symbols and Rs are lists of neighbors' symbols and Cartesian positions, respectively. Parameters ---------- index : int Index of the center atom. symbol : str Symbol of the center atom. n_symbols : list of str List of neighbors' symbols. Rs : list of list of float List of Cartesian atomic positions of neighbors. Returns ------- symbols, fingerprints : list of float Fingerprints for atom specified by its index and symbol. """ home = self.atoms[index].position cutoff = self.cutoff Rc = cutoff['kwargs']['Rc'] if cutoff['name'] == 'Cosine': cutoff_fxn = Cosine(Rc) elif cutoff['name'] == 'Polynomial': p_gamma = cutoff['kwargs']['gamma'] cutoff_fxn = Polynomial(Rc, gamma=p_gamma) fingerprint = [] for n in range(self.globals.nmax + 1): for l in range(n + 1): if (n - l) % 2 == 0: norm = 0. for m in range(l + 1): c_nlm = 0. for n_symbol, neighbor in zip(n_symbols, Rs): x = (neighbor[0] - home[0]) / Rc y = (neighbor[1] - home[1]) / Rc z = (neighbor[2] - home[2]) / Rc rho = np.linalg.norm([x, y, z]) if self.fortran: c_args = [Rc * rho] if cutoff['name'] == 'Polynomial': c_args.append(p_gamma) Z_nlm = fmodules.calculate_z( n=n, l=l, m=m, x=x, y=y, z=z, factorial=self.factorial, length=len(self.factorial)) Z_nlm = self.globals.Gs[symbol][n_symbol] * \ Z_nlm * cutoff_fxn(*c_args) else: # Alternative ways to calculate Z_nlm # Z_nlm = self.globals.Gs[symbol][n_symbol] * \ # calculate_Z(n, l, m, x, y, z, # self.factorial) * \ # cutoff_fxn(rho * Rc) # Z_nlm = self.globals.Gs[symbol][n_symbol] * \ # calculate_Z2(n, l, m, x, y, z) * \ # cutoff_fxn(rho * Rc) if rho > 0.: theta = np.arccos(z / rho) else: theta = 0. if x < 0.: phi = np.pi + np.arctan(y / x) elif 0. < x and y < 0.: phi = 2 * np.pi + np.arctan(y / x) elif 0. < x and 0. <= y: phi = np.arctan(y / x) elif x == 0. and 0. < y: phi = 0.5 * np.pi elif x == 0. and y < 0.: phi = 1.5 * np.pi else: phi = 0. c_args = [Rc * rho] if cutoff['name'] == 'Polynomial': c_args.append(p_gamma) Z_nlm = self.globals.Gs[symbol][n_symbol] * \ calculate_R(n, l, rho, self.factorial) * \ sph_harm(m, l, phi, theta) * \ cutoff_fxn(*c_args) # sum over neighbors c_nlm += np.conjugate(Z_nlm) # sum over m values if m == 0: norm += c_nlm * np.conjugate(c_nlm) else: norm += 2. * c_nlm * np.conjugate(c_nlm) fingerprint.append(norm.real) return symbol, fingerprint class FingerprintPrimeCalculator: """For integration with .utilities.Data""" def __init__(self, neighborlist, Gs, nmax, cutoff, fortran): self.globals = Parameters({'cutoff': cutoff, 'Gs': Gs, 'nmax': nmax}) self.keyed = Parameters({'neighborlist': neighborlist}) self.parallel_command = 'calculate_fingerprint_primes' self.fortran = fortran try: # for scipy v <= 0.90 from scipy import factorial as fac except ImportError: try: # for scipy v >= 0.10 from scipy.misc import factorial as fac except ImportError: # for newer version of scipy from scipy.special import factorial as fac self.factorial = [fac(0.5 * _) for _ in range(4 * nmax + 3)] def calculate(self, image, key): """Makes a list of fingerprint derivatives, one per atom, for the fed image. Parameters --------- image : object ASE atoms object. key : str Key of the image after being hashed. """ self.atoms = image nl = self.keyed.neighborlist[key] fingerprintprimes = {} for atom in image: selfsymbol = atom.symbol selfindex = atom.index selfneighborindices, selfneighboroffsets = nl[selfindex] selfneighborsymbols = [ image[_].symbol for _ in selfneighborindices] for i in range(3): # Calculating derivative of self atom fingerprints w.r.t. # coordinates of itself. nneighborindices, nneighboroffsets = nl[selfindex] nneighborsymbols = [image[_].symbol for _ in nneighborindices] Rs = [image.positions[_index] + np.dot(_offset, image.get_cell()) for _index, _offset in zip(nneighborindices, nneighboroffsets)] der_indexfp = self.get_fingerprintprime( selfindex, selfsymbol, nneighborindices, nneighborsymbols, Rs, selfindex, i) fingerprintprimes[ (selfindex, selfsymbol, selfindex, selfsymbol, i)] = \ der_indexfp # Calculating derivative of neighbor atom fingerprints w.r.t. # coordinates of self atom. for nindex, nsymbol, noffset in \ zip(selfneighborindices, selfneighborsymbols, selfneighboroffsets): # for calculating forces, summation runs over neighbor # atoms of type II (within the main cell only) if noffset.all() == 0: nneighborindices, nneighboroffsets = nl[nindex] nneighborsymbols = \ [image[_].symbol for _ in nneighborindices] Rs = [image.positions[_index] + np.dot(_offset, image.get_cell()) for _index, _offset in zip(nneighborindices, nneighboroffsets)] # for calculating derivatives of fingerprints, # summation runs over neighboring atoms of type # I (either inside or outside the main cell) der_indexfp = self.get_fingerprintprime( nindex, nsymbol, nneighborindices, nneighborsymbols, Rs, selfindex, i) fingerprintprimes[ (selfindex, selfsymbol, nindex, nsymbol, i)] = \ der_indexfp return fingerprintprimes def get_fingerprintprime(self, index, symbol, n_indices, n_symbols, Rs, p, q): """Returns the value of the derivative of G for atom with index and symbol with respect to coordinate x_{i} of atom index m. n_indices, n_symbols and Rs are lists of neighbors' indices, symbols and Cartesian positions, respectively. Parameters ---------- index : int Index of the center atom. symbol : str Symbol of the center atom. n_indices : list of int List of neighbors' indices. n_symbols : list of str List of neighbors' symbols. Rs : list of list of float List of Cartesian atomic positions. p : int Index of the pair atom. q : int Direction of the derivative; is an integer from 0 to 2. Returns ------- fingerprint_prime : list of float The value of the derivative of the fingerprints for atom with index and symbol with respect to coordinate x_{i} of atom index m. """ home = self.atoms[index].position cutoff = self.globals.cutoff Rc = cutoff['kwargs']['Rc'] if cutoff['name'] is 'Cosine': cutoff_fxn = Cosine(Rc) elif cutoff['name'] is 'Polynomial': p_gamma = cutoff['kwargs']['gamma'] cutoff_fxn = Polynomial(Rc, gamma=p_gamma) fingerprint_prime = [] for n in range(self.globals.nmax + 1): for l in range(n + 1): if (n - l) % 2 == 0: if self.fortran: # fortran version; faster G_numbers = [self.globals.Gs[symbol][elm] for elm in n_symbols] numbers = [atomic_numbers[elm] for elm in n_symbols] if len(Rs) == 0: norm_prime = 0. else: args_calculate_zernike_prime = dict( n=n, l=l, n_length=len(n_indices), n_indices=list(n_indices), numbers=numbers, rs=Rs, g_numbers=G_numbers, cutoff=Rc, cutofffn=cutoff['name'], indexx=index, home=home, p=p, q=q, fac_length=len(self.factorial), factorial=self.factorial) if cutoff['name'] == 'Polynomial': args_calculate_zernike_prime['p_gamma'] =\ cutoff['kwargs']['gamma'] norm_prime = \ fmodules.calculate_zernike_prime( **args_calculate_zernike_prime) else: norm_prime = 0. for m in range(l + 1): c_nlm = 0. c_nlm_prime = 0. for n_index, n_symbol, neighbor in zip(n_indices, n_symbols, Rs): x = (neighbor[0] - home[0]) / Rc y = (neighbor[1] - home[1]) / Rc z = (neighbor[2] - home[2]) / Rc rho = np.linalg.norm([x, y, z]) c_args = [rho * Rc] if cutoff['name'] == 'Polynomial': c_args.append(p_gamma) _Z_nlm = calculate_Z(n, l, m, x, y, z, self.factorial) # Calculates Z_nlm Z_nlm = _Z_nlm * \ cutoff_fxn(*c_args) # Calculates Z_nlm_prime Z_nlm_prime = _Z_nlm * \ cutoff_fxn.prime(*c_args) * \ der_position( index, n_index, home, neighbor, p, q) _Z_nlm_prime = calculate_Z_prime( n, l, m, x, y, z, q, self.factorial) if (Kronecker(n_index, p) - Kronecker(index, p)) == 1: Z_nlm_prime += \ cutoff_fxn(*c_args) * \ _Z_nlm_prime / Rc elif (Kronecker(n_index, p) - Kronecker(index, p)) == -1: Z_nlm_prime -= \ cutoff_fxn(*c_args) * \ _Z_nlm_prime / Rc # sum over neighbors c_nlm += self.globals.Gs[symbol][ n_symbol] * np.conjugate(Z_nlm) c_nlm_prime += self.globals.Gs[symbol][ n_symbol] * np.conjugate(Z_nlm_prime) # sum over m values if m == 0: norm_prime += 2. * c_nlm * \ np.conjugate(c_nlm_prime) else: norm_prime += 4. * c_nlm * \ np.conjugate(c_nlm_prime) fingerprint_prime.append(norm_prime.real) return fingerprint_prime # Auxiliary functions ######################################################### def binomial(n, k, factorial): """ Returns C(n,k) = n!/(k!(n-k)!). """ assert n >= 0 and k >= 0 and n >= k, \ 'n and k should be non-negative integers with n >= k.' c = factorial[int(2 * n)] / \ (factorial[int(2 * k)] * factorial[int(2 * (n - k))]) return c def calculate_R(n, l, rho, factorial): """Calculates R_{n}^{l}(rho) according to the last equation of wikipedia. """ if (n - l) % 2 != 0: return 0 else: value = 0. k = (n - l) / 2 term1 = np.sqrt(2. * n + 3.) for s in range(int(k) + 1): b1 = binomial(k, s, factorial) b2 = binomial(n - s - 1 + 1.5, k, factorial) value += ((-1) ** s) * b1 * b2 * (rho ** (n - 2. * s)) value *= term1 return value def generate_coefficients(elements): """Automatically generates coefficients if not given by the user. Parameters ---------- elements : list of str List of symbols of all atoms. Returns ------- G : dict of dicts """ _G = {} for element in elements: _G[element] = atomic_numbers[element] G = {} for element in elements: G[element] = _G return G def Kronecker(i, j): """Kronecker delta function. i : int First index of Kronecker delta. j : int Second index of Kronecker delta. Returns ------- Kronecker delta : int """ if i == j: return 1 else: return 0 def der_position(m, n, Rm, Rn, l, i): """Returns the derivative of the norm of position vector R_{mn} with respect to x_{i} of atomic index l. Parameters ---------- m : int Index of the first atom. n : int Index of the second atom. Rm : float Position of the first atom. Rn : float Position of the second atom. l : int Index of the atom force is acting on. i : int Direction of force. Returns ------- der_position : list of float The derivative of the norm of position vector R_{mn} with respect to x_{i} of atomic index l. """ Rmn = np.linalg.norm(Rm - Rn) # mm != nn is necessary for periodic systems if l == m and m != n: der_position = (Rm[i] - Rn[i]) / Rmn elif l == n and m != n: der_position = -(Rm[i] - Rn[i]) / Rmn else: der_position = 0. return der_position def calculate_q(nu, k, l, factorial): """Calculates q_{kl}^{nu} according to the unnumbered equation afer Eq. (7) of "3D Zernike Descriptors for Content Based Shape Retrieval", Computer-Aided Design 36 (2004) 1047-1062. """ result = ((-1) ** (k + nu)) * sqrt((2. * l + 4. * k + 3.) / 3.) * \ binomial(k, nu, factorial) * \ binomial(2. * k, k, factorial) * \ binomial(2. * (k + l + nu) + 1., 2. * k, factorial) / \ binomial(k + l + nu, k, factorial) / (2. ** (2. * k)) return result def calculate_Z(n, l, m, x, y, z, factorial): """Calculates Z_{nl}^{m}(x, y, z) according to the unnumbered equation afer Eq. (11) of "3D Zernike Descriptors for Content Based Shape Retrieval", Computer-Aided Design 36 (2004) 1047-1062. """ value = 0. term1 = sqrt((2. * l + 1.) * factorial[int(2 * (l + m))] * factorial[int(2 * (l - m))]) / factorial[int(2 * l)] term2 = 2. ** (-m) k = int((n - l) / 2.) for nu in range(k + 1): q = calculate_q(nu, k, l, factorial) for alpha in range(nu + 1): b1 = binomial(nu, alpha, factorial) for beta in range(nu - alpha + 1): b2 = binomial(nu - alpha, beta, factorial) term3 = q * b1 * b2 for u in range(m + 1): b5 = binomial(m, u, factorial) term4 = ((-1.)**(m - u)) * b5 * (1j**u) for mu in range(int((l - m) / 2.) + 1): b6 = binomial(l, mu, factorial) b7 = binomial(l - mu, m + mu, factorial) term5 = ((-1.)**mu) * (2.**(-2. * mu)) * b6 * b7 for eta in range(mu + 1): r = 2. * (eta + alpha) + u s = 2. * (mu - eta + beta) + m - u t = 2. * (nu - alpha - beta - mu) + l - m value += term3 * term4 * term5 * \ binomial(mu, eta, factorial) * \ (x ** r) * (y ** s) * (z ** t) term6 = (1j) ** m value = term1 * term2 * term6 * value value = value / sqrt(4. * np.pi / 3.) return value def calculate_Z_prime(n, l, m, x, y, z, p, factorial): """Calculates dZ_{nl}^{m}(x, y, z)/dR_{p} according to the unnumbered equation afer Eq. (11) of "3D Zernike Descriptors for Content Based Shape Retrieval", Computer-Aided Design 36 (2004) 1047-1062. """ value = 0. term1 = sqrt((2. * l + 1.) * factorial[int(2 * (l + m))] * factorial[int(2 * (l - m))]) / factorial[int(2 * l)] term2 = 2. ** (-m) k = int((n - l) / 2.) for nu in range(k + 1): q = calculate_q(nu, k, l, factorial) for alpha in range(nu + 1): b1 = binomial(nu, alpha, factorial) for beta in range(nu - alpha + 1): b2 = binomial(nu - alpha, beta, factorial) term3 = q * b1 * b2 for u in range(m + 1): term4 = ((-1.)**(m - u)) * binomial( m, u, factorial) * (1j**u) for mu in range(int((l - m) / 2.) + 1): term5 = ((-1.)**mu) * (2.**(-2. * mu)) * \ binomial(l, mu, factorial) * \ binomial(l - mu, m + mu, factorial) for eta in range(mu + 1): r = 2 * (eta + alpha) + u s = 2 * (mu - eta + beta) + m - u t = 2 * (nu - alpha - beta - mu) + l - m coefficient = term3 * term4 * \ term5 * binomial(mu, eta, factorial) if p == 0: if r != 0: value += coefficient * r * \ (x ** (r - 1)) * (y ** s) * (z ** t) elif p == 1: if s != 0: value += coefficient * s * \ (x ** r) * (y ** (s - 1)) * (z ** t) elif p == 2: if t != 0: value += coefficient * t * \ (x ** r) * (y ** s) * (z ** (t - 1)) term6 = (1j) ** m value = term1 * term2 * term6 * value value = value / sqrt(4. * np.pi / 3.) return value if __name__ == "__main__": """Directly calling this module; apparently from another node. Calls should come as python -m amp.descriptor.example id hostname:port This session will then start a zmq session with that socket, labeling itself with id. Instructions on what to do will come from the socket. """ import sys import tempfile import zmq from ..utilities import MessageDictionary fortran = False if fmodules is None else True hostsocket = sys.argv[-1] proc_id = sys.argv[-2] msg = MessageDictionary(proc_id) # Send standard lines to stdout signaling process started and where # error is directed. This should be caught by pxssh. (This could # alternatively be done by zmq, but this works.) print('') # Signal that program started. sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('Log and error written to %s' % sys.stderr.name) def w(text): """Writes to stderr and flushes.""" sys.stderr.write(text + '\n') sys.stderr.flush() # Establish client session via zmq; find purpose. context = zmq.Context() w('Context started.') socket = context.socket(zmq.REQ) w('Socket started.') socket.connect('tcp://%s' % hostsocket) w('Connection made.') socket.send_pyobj(msg('')) w('Message sent.') purpose = socket.recv_pyobj() w('Purpose received: {}.'.format(purpose)) if purpose == 'calculate_neighborlists': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() # sys.stderr.write(str(images)) # Just to see if they are there. # Perform the calculations. calc = NeighborlistCalculator(cutoff=cutoff) neighborlist = {} # for key in images.iterkeys(): while len(images) > 0: key, image = images.popitem() # Reduce memory. neighborlist[key] = calc.calculate(image, key) # Send the results. socket.send_pyobj(msg('', neighborlist)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprints': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'Gs')) Gs = socket.recv_pyobj() socket.send_pyobj(msg('', 'nmax')) nmax = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() w('Received images and parameters.') calc = FingerprintCalculator(neighborlist, Gs, nmax, cutoff, fortran) w('Established calculator. Calculating.') result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. w('Sending results.') socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. elif purpose == 'calculate_fingerprint_primes': # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'Gs')) Gs = socket.recv_pyobj() socket.send_pyobj(msg('', 'nmax')) nmax = socket.recv_pyobj() socket.send_pyobj(msg('', 'neighborlist')) neighborlist = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() calc = FingerprintPrimeCalculator(neighborlist, Gs, nmax, cutoff, fortran) result = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. result[key] = calc.calculate(image, key) if len(images) % 100 == 0: socket.send_pyobj(msg('', len(images))) socket.recv_string() # Needed to complete REQ/REP. # Send the results. socket.send_pyobj(msg('', result)) socket.recv_string() # Needed to complete REQ/REP. else: raise NotImplementedError('purpose %s unknown.' % purpose) andrewpeterson-amp-4878fc892f2c/amp/model.f90000066400000000000000000001026151332417112400206500ustar00rootroot00000000000000!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Fortran Version = 9 subroutine check_version(version, warning) implicit none integer:: version, warning !f2py intent(in):: version !f2py intent(out):: warning if (version .NE. 9) then warning = 1 else warning = 0 end if end subroutine !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! module containing all the data of fingerprints (should be fed in ! by python) module fingerprint_props implicit none integer, allocatable:: num_fingerprints_of_elements(:) double precision, allocatable:: raveled_fingerprints(:, :) double precision, allocatable:: raveled_fingerprintprimes(:, :) end module fingerprint_props !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! module containing model data (should be fed in by python) module model_props implicit none ! mode_signal is 1 for image-centered mode, and 2 for ! atom-centered mode integer:: mode_signal logical:: train_forces double precision:: energy_coefficient double precision:: force_coefficient double precision:: overfit logical:: numericprime double precision:: d end module model_props !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! module containing all the data of images (should be fed in by ! python) module images_props implicit none integer:: num_images ! atom-centered variables integer:: num_elements integer, allocatable:: elements_numbers(:) integer, allocatable:: num_images_atoms(:) integer, allocatable:: atomic_numbers(:) integer, allocatable:: num_neighbors(:) integer, allocatable:: raveled_neighborlists(:) double precision, allocatable:: actual_energies(:) double precision, allocatable:: actual_forces(:, :) ! image-centered variables integer:: num_atoms double precision, allocatable:: atomic_positions(:, :) end module images_props !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! subroutine that calculates the loss function and its prime subroutine calculate_loss(parameters, num_parameters, & lossprime, loss, dloss_dparameters, energyloss, forceloss, & energy_maxresid, force_maxresid) use images_props use fingerprint_props use model_props use neuralnetwork !!!!!!!!!!!!!!!!!!!!!!!! input/output variables !!!!!!!!!!!!!!!!!!!!!!!! integer:: num_parameters double precision:: parameters(num_parameters) logical:: lossprime double precision:: loss, energyloss, forceloss double precision:: energy_maxresid, force_maxresid double precision:: dloss_dparameters(num_parameters) !f2py intent(in):: parameters, num_parameters !f2py intent(in):: lossprime !f2py intent(out):: loss, energyloss, forceloss !f2py intent(out):: energy_maxresid, force_maxresid !f2py intent(out):: dloss_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!! type definition !!!!!!!!!!!!!!!!!!!!!!!!!!!! type:: image_forces sequence double precision, allocatable:: atom_forces(:, :) end type image_forces type:: integer_one_d_array sequence integer, allocatable:: onedarray(:) end type integer_one_d_array type:: embedded_real_one_one_d_array sequence type(real_one_d_array), allocatable:: onedarray(:) end type embedded_real_one_one_d_array type:: embedded_real_one_two_d_array sequence type(real_two_d_array), allocatable:: onedarray(:) end type embedded_real_one_two_d_array type:: embedded_integer_one_one_d_array sequence type(integer_one_d_array), allocatable:: onedarray(:) end type embedded_integer_one_one_d_array type:: embedded_one_one_two_d_array sequence type(embedded_real_one_two_d_array), allocatable:: onedarray(:) end type embedded_one_one_two_d_array !!!!!!!!!!!!!!!!!!!!!!!!!! dummy variables !!!!!!!!!!!!!!!!!!!!!!!!!!!!! double precision, allocatable:: fingerprint(:) type(embedded_real_one_one_d_array), allocatable:: & unraveled_fingerprints(:) type(integer_one_d_array), allocatable:: & unraveled_atomic_numbers(:) double precision:: amp_energy, actual_energy, atom_energy double precision:: residual_per_atom, dforce, force_resid double precision:: overfitloss integer:: i, index, j, p, k, q, l, m, & len_of_fingerprint, symbol, element, image_no, num_inputs double precision:: denergy_dparameters(num_parameters) double precision:: daenergy_dparameters(num_parameters) double precision:: dforce_dparameters(num_parameters) double precision:: doverfitloss_dparameters(num_parameters) type(real_two_d_array), allocatable:: dforces_dparameters(:) type(image_forces), allocatable:: unraveled_actual_forces(:) type(embedded_integer_one_one_d_array), allocatable:: & unraveled_neighborlists(:) type(embedded_one_one_two_d_array), allocatable:: & unraveled_fingerprintprimes(:) double precision, allocatable:: fingerprintprime(:) integer:: nindex, nsymbol, selfindex double precision, allocatable:: & actual_forces_(:, :), amp_forces(:, :) integer, allocatable:: neighborindices(:) ! image-centered mode type(real_one_d_array), allocatable:: & unraveled_atomic_positions(:) double precision, allocatable:: inputs(:), inputs_(:) !!!!!!!!!!!!!!!!!!!!!!!!!!!! calculations !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! if (mode_signal == 1) then allocate(inputs(3 * num_atoms)) allocate(inputs_(3 * num_atoms)) allocate(unraveled_atomic_positions(num_images)) call unravel_atomic_positions() else allocate(unraveled_fingerprints(num_images)) allocate(unraveled_atomic_numbers(num_images)) allocate(unraveled_neighborlists(num_images)) allocate(unraveled_fingerprintprimes(num_images)) call unravel_atomic_numbers() call unravel_fingerprints() end if if (train_forces .EQV. .TRUE.) then allocate(unraveled_actual_forces(num_images)) call unravel_actual_forces() if (mode_signal == 2) then call unravel_neighborlists() call unravel_fingerprintprimes() end if end if energyloss = 0.0d0 forceloss = 0.0d0 energy_maxresid = 0.0d0 force_maxresid = 0.0d0 do j = 1, num_parameters dloss_dparameters(j) = 0.0d0 end do ! summation over images do image_no = 1, num_images if (mode_signal == 1) then num_inputs = 3 * num_atoms inputs = unraveled_atomic_positions(image_no)%onedarray else num_atoms = num_images_atoms(image_no) end if actual_energy = actual_energies(image_no) ! calculates amp_energy call calculate_energy(image_no) ! calculates energy_maxresid residual_per_atom = ABS(amp_energy - actual_energy) / num_atoms if (residual_per_atom .GT. energy_maxresid) then energy_maxresid = residual_per_atom end if ! calculates energyloss energyloss = energyloss + residual_per_atom ** 2.0d0 if (lossprime .EQV. .TRUE.) then ! calculates denergy_dparameters if (mode_signal == 1) then ! image-centered mode denergy_dparameters = & calculate_denergy_dparameters_(num_inputs, inputs, & num_parameters, parameters) else ! atom-centered mode do j = 1, num_parameters denergy_dparameters(j) = 0.0d0 end do if (numericprime .EQV. .FALSE.) then call calculate_denergy_dparameters(image_no) else call calculate_numerical_denergy_dparameters(image_no) end if end if ! calculates contribution of energyloss to dloss_dparameters do j = 1, num_parameters dloss_dparameters(j) = dloss_dparameters(j) + & energy_coefficient * 2.0d0 * & (amp_energy - actual_energy) * & denergy_dparameters(j) / (num_atoms ** 2.0d0) end do end if if (train_forces .EQV. .TRUE.) then allocate(actual_forces_(num_atoms, 3)) do selfindex = 1, num_atoms do i = 1, 3 actual_forces_(selfindex, i) = & unraveled_actual_forces(& image_no)%atom_forces(selfindex, i) end do end do ! calculates amp_forces call calculate_forces(image_no) ! calculates forceloss do selfindex = 1, num_atoms do i = 1, 3 forceloss = forceloss + & (1.0d0 / 3.0d0) * (amp_forces(selfindex, i) - & actual_forces_(selfindex, i)) ** 2.0d0 / num_atoms end do end do ! calculates force_maxresid do selfindex = 1, num_atoms do i = 1, 3 force_resid = & ABS(amp_forces(selfindex, i) - & actual_forces_(selfindex, i)) if (force_resid .GT. force_maxresid) then force_maxresid = force_resid end if end do end do if (lossprime .EQV. .TRUE.) then allocate(dforces_dparameters(num_atoms)) do selfindex = 1, num_atoms allocate(dforces_dparameters(& selfindex)%twodarray(3, num_parameters)) do i = 1, 3 do j = 1, num_parameters dforces_dparameters(& selfindex)%twodarray(i, j) = 0.0d0 end do end do end do ! calculates dforces_dparameters if (numericprime .EQV. .FALSE.) then call calculate_dforces_dparameters(image_no) else call calculate_numerical_dforces_dparameters(image_no) end if ! calculates contribution of forceloss to ! dloss_dparameters do selfindex = 1, num_atoms do i = 1, 3 do j = 1, num_parameters dloss_dparameters(j) = & dloss_dparameters(j) + & force_coefficient * (2.0d0 / 3.0d0) * & (amp_forces(selfindex, i) - & actual_forces_(selfindex, i)) * & dforces_dparameters(& selfindex)%twodarray(i, j) / num_atoms end do end do end do do p = 1, size(dforces_dparameters) deallocate(dforces_dparameters(p)%twodarray) end do deallocate(dforces_dparameters) end if deallocate(actual_forces_) deallocate(amp_forces) end if end do loss = energy_coefficient * energyloss + & force_coefficient * forceloss ! if overfit coefficient is more than zero, overfit ! contribution to loss and dloss_dparameters is also added. if (overfit .GT. 0.0d0) then overfitloss = 0.0d0 do j = 1, num_parameters overfitloss = overfitloss + & parameters(j) ** 2.0d0 end do overfitloss = overfit * overfitloss loss = loss + overfitloss do j = 1, num_parameters doverfitloss_dparameters(j) = & 2.0d0 * overfit * parameters(j) dloss_dparameters(j) = dloss_dparameters(j) + & doverfitloss_dparameters(j) end do end if ! deallocations for all images if (mode_signal == 1) then do image_no = 1, num_images deallocate(unraveled_atomic_positions(image_no)%onedarray) end do deallocate(unraveled_atomic_positions) deallocate(inputs) deallocate(inputs_) else do image_no = 1, num_images deallocate(unraveled_atomic_numbers(image_no)%onedarray) end do deallocate(unraveled_atomic_numbers) do image_no = 1, num_images num_atoms = num_images_atoms(image_no) do index = 1, num_atoms deallocate(unraveled_fingerprints(& image_no)%onedarray(index)%onedarray) end do deallocate(unraveled_fingerprints(image_no)%onedarray) end do deallocate(unraveled_fingerprints) end if if (train_forces .EQV. .TRUE.) then do image_no = 1, num_images deallocate(unraveled_actual_forces(image_no)%atom_forces) end do deallocate(unraveled_actual_forces) if (mode_signal == 2) then do image_no = 1, num_images num_atoms = num_images_atoms(image_no) do selfindex = 1, num_atoms do nindex = 1, & size(unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray) deallocate(& unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray(& nindex)%twodarray) end do deallocate(unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray) end do deallocate(unraveled_fingerprintprimes(& image_no)%onedarray) end do deallocate(unraveled_fingerprintprimes) do image_no = 1, num_images num_atoms = num_images_atoms(image_no) do index = 1, num_atoms deallocate(unraveled_neighborlists(& image_no)%onedarray(index)%onedarray) end do deallocate(unraveled_neighborlists(image_no)%onedarray) end do deallocate(unraveled_neighborlists) end if end if contains !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates amp_energy subroutine calculate_energy(image_no) if (mode_signal == 1) then amp_energy = & calculate_image_energy(num_inputs, inputs, num_parameters, & parameters) else amp_energy = 0.0d0 do index = 1, num_atoms symbol = unraveled_atomic_numbers(& image_no)%onedarray(index) do element = 1, num_elements if (symbol == elements_numbers(element)) then exit end if end do len_of_fingerprint = num_fingerprints_of_elements(element) allocate(fingerprint(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprint(p) = & unraveled_fingerprints(& image_no)%onedarray(index)%onedarray(p) end do atom_energy = calculate_atomic_energy(symbol, & len_of_fingerprint, fingerprint, num_elements, & elements_numbers, num_parameters, parameters) deallocate(fingerprint) amp_energy = amp_energy + atom_energy end do end if end subroutine calculate_energy !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates amp_forces subroutine calculate_forces(image_no) allocate(amp_forces(num_atoms, 3)) do selfindex = 1, num_atoms do i = 1, 3 amp_forces(selfindex, i) = 0.0d0 end do end do do selfindex = 1, num_atoms if (mode_signal == 1) then do i = 1, 3 do p = 1, 3 * num_atoms inputs_(p) = 0.0d0 end do inputs_(3 * (selfindex - 1) + i) = 1.0d0 amp_forces(selfindex, i) = calculate_force_(num_inputs, & inputs, inputs_, num_parameters, parameters) end do else ! neighborindices list is generated. allocate(neighborindices(size(& unraveled_neighborlists(image_no)%onedarray(& selfindex)%onedarray))) do p = 1, size(unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray) neighborindices(p) = unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray(p) end do do l = 1, size(neighborindices) nindex = neighborindices(l) nsymbol = unraveled_atomic_numbers(& image_no)%onedarray(nindex) do element = 1, num_elements if (nsymbol == elements_numbers(element)) then exit end if end do len_of_fingerprint = & num_fingerprints_of_elements(element) allocate(fingerprint(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprint(p) = unraveled_fingerprints(& image_no)%onedarray(nindex)%onedarray(p) end do do i = 1, 3 allocate(fingerprintprime(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprintprime(p) = & unraveled_fingerprintprimes(& image_no)%onedarray(& selfindex)%onedarray(l)%twodarray(i, p) end do dforce = calculate_force(nsymbol, len_of_fingerprint, & fingerprint, fingerprintprime, & num_elements, elements_numbers, & num_parameters, parameters) amp_forces(selfindex, i) = & amp_forces(selfindex, i) + dforce deallocate(fingerprintprime) end do deallocate(fingerprint) end do deallocate(neighborindices) end if end do end subroutine calculate_forces !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates analytical denergy_dparameters in ! the atom-centered mode. subroutine calculate_denergy_dparameters(image_no) do index = 1, num_atoms symbol = unraveled_atomic_numbers(image_no)%onedarray(index) do element = 1, num_elements if (symbol == elements_numbers(element)) then exit end if end do len_of_fingerprint = num_fingerprints_of_elements(element) allocate(fingerprint(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprint(p) = unraveled_fingerprints(& image_no)%onedarray(index)%onedarray(p) end do daenergy_dparameters = calculate_datomicenergy_dparameters(& symbol, len_of_fingerprint, fingerprint, & num_elements, elements_numbers, num_parameters, parameters) deallocate(fingerprint) do j = 1, num_parameters denergy_dparameters(j) = denergy_dparameters(j) + & daenergy_dparameters(j) end do end do end subroutine calculate_denergy_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates numerical denergy_dparameters in the ! atom-centered mode. subroutine calculate_numerical_denergy_dparameters(image_no) double precision:: eplus, eminus do j = 1, num_parameters parameters(j) = parameters(j) + d call calculate_energy(image_no) eplus = amp_energy parameters(j) = parameters(j) - 2.0d0 * d call calculate_energy(image_no) eminus = amp_energy denergy_dparameters(j) = (eplus - eminus) / (2.0d0 * d) parameters(j) = parameters(j) + d end do call calculate_energy(image_no) end subroutine calculate_numerical_denergy_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates dforces_dparameters. subroutine calculate_dforces_dparameters(image_no) if (mode_signal == 1) then ! image-centered mode do selfindex = 1, num_atoms do i = 1, 3 do p = 1, 3 * num_atoms inputs_(p) = 0.0d0 end do inputs_(3 * (selfindex - 1) + i) = 1.0d0 dforce_dparameters = calculate_dforce_dparameters_(& num_inputs, inputs, inputs_, num_parameters, parameters) do j = 1, num_parameters dforces_dparameters(selfindex)%twodarray(i, j) = & dforce_dparameters(j) end do end do end do else ! atom-centered mode do selfindex = 1, num_atoms ! neighborindices list is generated. allocate(neighborindices(size(& unraveled_neighborlists(image_no)%onedarray(& selfindex)%onedarray))) do p = 1, size(unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray) neighborindices(p) = unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray(p) end do do l = 1, size(neighborindices) nindex = neighborindices(l) nsymbol = unraveled_atomic_numbers(& image_no)%onedarray(nindex) do element = 1, num_elements if (nsymbol == elements_numbers(element)) then exit end if end do len_of_fingerprint = & num_fingerprints_of_elements(element) allocate(fingerprint(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprint(p) = unraveled_fingerprints(& image_no)%onedarray(nindex)%onedarray(p) end do do i = 1, 3 allocate(fingerprintprime(len_of_fingerprint)) do p = 1, len_of_fingerprint fingerprintprime(p) = & unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray(& l)%twodarray(i, p) end do dforce_dparameters = calculate_dforce_dparameters(& nsymbol, len_of_fingerprint, fingerprint, & fingerprintprime, num_elements, & elements_numbers, num_parameters, parameters) deallocate(fingerprintprime) do j = 1, num_parameters dforces_dparameters(& selfindex)%twodarray(i, j) = & dforces_dparameters(& selfindex)%twodarray(i, j) + & dforce_dparameters(j) end do end do deallocate(fingerprint) end do deallocate(neighborindices) end do end if end subroutine calculate_dforces_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! calculates numerical dforces_dparameters in the ! atom-centered mode. subroutine calculate_numerical_dforces_dparameters(image_no) double precision, allocatable:: fplus(:, :), fminus(:, :) do j = 1, num_parameters parameters(j) = parameters(j) + d deallocate(amp_forces) call calculate_forces(image_no) allocate(fplus(num_atoms, 3)) do selfindex = 1, num_atoms do i = 1, 3 fplus(selfindex, i) = amp_forces(selfindex, i) end do end do parameters(j) = parameters(j) - 2.0d0 * d deallocate(amp_forces) call calculate_forces(image_no) allocate(fminus(num_atoms, 3)) do selfindex = 1, num_atoms do i = 1, 3 fminus(selfindex, i) = amp_forces(selfindex, i) end do end do do selfindex = 1, num_atoms do i = 1, 3 dforces_dparameters(selfindex)%twodarray(i, j) = & (fplus(selfindex, i) - fminus(selfindex, i)) / & (2.0d0 * d) end do end do parameters(j) = parameters(j) + d deallocate(fplus) deallocate(fminus) end do deallocate(amp_forces) call calculate_forces(image_no) end subroutine calculate_numerical_dforces_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! used only in the image-centered mode. subroutine unravel_atomic_positions() do image_no = 1, num_images allocate(unraveled_atomic_positions(image_no)%onedarray(& 3 * num_atoms)) do index = 1, num_atoms do i = 1, 3 unraveled_atomic_positions(image_no)%onedarray(& 3 * (index - 1) + i) = atomic_positions(& image_no, 3 * (index - 1) + i) end do end do end do end subroutine unravel_atomic_positions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine unravel_atomic_numbers() k = 0 do image_no = 1, num_images num_atoms = num_images_atoms(image_no) allocate(unraveled_atomic_numbers(& image_no)%onedarray(num_atoms)) do l = 1, num_atoms unraveled_atomic_numbers(image_no)%onedarray(l) & = atomic_numbers(k + l) end do k = k + num_atoms end do end subroutine unravel_atomic_numbers !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine unravel_neighborlists() k = 0 q = 0 do image_no = 1, num_images num_atoms = num_images_atoms(image_no) allocate(unraveled_neighborlists(image_no)%onedarray(& num_atoms)) do index = 1, num_atoms allocate(unraveled_neighborlists(image_no)%onedarray(& index)%onedarray(num_neighbors(k + index))) do p = 1, num_neighbors(k + index) unraveled_neighborlists(image_no)%onedarray(& index)%onedarray(p) = raveled_neighborlists(q + p)+1 end do q = q + num_neighbors(k + index) end do k = k + num_atoms end do end subroutine unravel_neighborlists !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine unravel_actual_forces() k = 0 do image_no = 1, num_images if (mode_signal == 1) then num_atoms = num_atoms else num_atoms = num_images_atoms(image_no) end if allocate(unraveled_actual_forces(image_no)%atom_forces(& num_atoms, 3)) do index = 1, num_atoms do i = 1, 3 unraveled_actual_forces(image_no)%atom_forces(& index, i) = actual_forces(k + index, i) end do end do k = k + num_atoms end do end subroutine unravel_actual_forces !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine unravel_fingerprints() k = 0 do image_no = 1, num_images num_atoms = & num_images_atoms(image_no) allocate(unraveled_fingerprints(& image_no)%onedarray(num_atoms)) do index = 1, num_atoms do element = 1, num_elements if (unraveled_atomic_numbers(& image_no)%onedarray(index)== & elements_numbers(element)) then allocate(unraveled_fingerprints(& image_no)%onedarray(index)%onedarray(& num_fingerprints_of_elements(element))) exit end if end do do l = 1, num_fingerprints_of_elements(element) unraveled_fingerprints(& image_no)%onedarray(index)%onedarray(l) = & raveled_fingerprints(k + index, l) end do end do k = k + num_atoms end do end subroutine unravel_fingerprints !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! subroutine unravel_fingerprintprimes() integer:: no_of_neighbors k = 0 m = 0 do image_no = 1, num_images num_atoms = & num_images_atoms(image_no) allocate(unraveled_fingerprintprimes(& image_no)%onedarray(num_atoms)) do selfindex = 1, num_atoms ! neighborindices list is generated. allocate(neighborindices(size(unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray))) do p = 1, size(unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray) neighborindices(p) = unraveled_neighborlists(& image_no)%onedarray(selfindex)%onedarray(p) end do no_of_neighbors = num_neighbors(k + selfindex) allocate(unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray(no_of_neighbors)) do nindex = 1, no_of_neighbors do nsymbol = 1, num_elements if (unraveled_atomic_numbers(& image_no)%onedarray(neighborindices(nindex)) == & elements_numbers(nsymbol)) then exit end if end do allocate(unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray(& nindex)%twodarray(3, num_fingerprints_of_elements(& nsymbol))) do p = 1, 3 do q = 1, num_fingerprints_of_elements(nsymbol) unraveled_fingerprintprimes(& image_no)%onedarray(selfindex)%onedarray(& nindex)%twodarray(p, q) = & raveled_fingerprintprimes(& 3 * m + 3 * nindex + p - 3, q) end do end do end do deallocate(neighborindices) m = m + no_of_neighbors end do k = k + num_atoms end do end subroutine unravel_fingerprintprimes !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! end subroutine calculate_loss !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! subroutine that deallocates variables subroutine deallocate_variables() use images_props use fingerprint_props use model_props use neuralnetwork !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! deallocating fingerprint_props if (allocated(num_fingerprints_of_elements) .EQV. .TRUE.) then deallocate(num_fingerprints_of_elements) end if if (allocated(raveled_fingerprints) .EQV. .TRUE.) then deallocate(raveled_fingerprints) end if if (allocated(raveled_fingerprintprimes) .EQV. .TRUE.) then deallocate(raveled_fingerprintprimes) end if ! deallocating images_props if (allocated(elements_numbers) .EQV. .TRUE.) then deallocate(elements_numbers) end if if (allocated(num_images_atoms) .EQV. .TRUE.) then deallocate(num_images_atoms) end if if (allocated(atomic_numbers) .EQV. .TRUE.) then deallocate(atomic_numbers) end if if (allocated(num_neighbors) .EQV. .TRUE.) then deallocate(num_neighbors) end if if (allocated(raveled_neighborlists) .EQV. .TRUE.) then deallocate(raveled_neighborlists) end if if (allocated(actual_energies) .EQV. .TRUE.) then deallocate(actual_energies) end if if (allocated(actual_forces) .EQV. .TRUE.) then deallocate(actual_forces) end if if (allocated(atomic_positions) .EQV. .TRUE.) then deallocate(atomic_positions) end if ! deallocating neuralnetwork if (allocated(min_fingerprints) .EQV. .TRUE.) then deallocate(min_fingerprints) end if if (allocated(max_fingerprints) .EQV. .TRUE.) then deallocate(max_fingerprints) end if if (allocated(no_layers_of_elements) .EQV. .TRUE.) then deallocate(no_layers_of_elements) end if if (allocated(no_nodes_of_elements) .EQV. .TRUE.) then deallocate(no_nodes_of_elements) end if !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! end subroutine deallocate_variables !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! andrewpeterson-amp-4878fc892f2c/amp/model/000077500000000000000000000000001332417112400203235ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/model/Makefile000066400000000000000000000001131332417112400217560ustar00rootroot00000000000000neuralnetwork.mod: gfortran -c neuralnetwork.f90 cp neuralnetwork.mod .. andrewpeterson-amp-4878fc892f2c/amp/model/__init__.py000077500000000000000000001412761332417112400224520ustar00rootroot00000000000000import sys import numpy as np from ase.calculators.calculator import Parameters from ..utilities import (Logger, ConvergenceOccurred, make_sublists, now, setup_parallel) try: from .. import fmodules except ImportError: fmodules = None class Model(object): """Class that includes common methods between different models.""" @property def log(self): """Method to set or get a logger. Should be an instance of amp.utilities.Logger. Parameters ---------- log : Logger object Write function at which to log data. Note this must be a callable function. """ if hasattr(self, '_log'): return self._log if hasattr(self.parent, 'log'): return self.parent.log return Logger(None) @log.setter def log(self, log): self._log = log def tostring(self): """Returns an evaluatable representation of the calculator that can be used to re-establish the calculator.""" # Make sure numpy prints out enough data. np.set_printoptions(precision=30, threshold=999999999) return self.parameters.tostring() def calculate_energy(self, fingerprints): """Calculates the model-predicted energy for an image, based on its fingerprint. Parameters ---------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': self.atomic_energies = [] energy = 0.0 for index, (symbol, afp) in enumerate(fingerprints): atom_energy = self.calculate_atomic_energy(afp=afp, index=index, symbol=symbol) self.atomic_energies.append(atom_energy) energy += atom_energy return energy def calculate_forces(self, fingerprints, fingerprintprimes): """Calculates the model-predicted forces for an image, based on derivatives of fingerprints. Parameters ---------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. fingerprintprimes : dict Dictionary with images hashs as keys and the corresponding fingerprint derivatives as values. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': selfindices = set([key[0] for key in fingerprintprimes.keys()]) forces = np.zeros((len(selfindices), 3)) for key in fingerprintprimes.keys(): selfindex, selfsymbol, nindex, nsymbol, i = key derafp = fingerprintprimes[key] afp = fingerprints[nindex][1] dforce = self.calculate_force(afp=afp, derafp=derafp, nindex=nindex, nsymbol=nsymbol, direction=i,) forces[selfindex][i] += dforce return forces def calculate_dEnergy_dParameters(self, fingerprints): """Calculates a list of floats corresponding to the derivative of model-predicted energy of an image with respect to model parameters. Parameters ---------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': denergy_dparameters = None for index, (symbol, afp) in enumerate(fingerprints): temp = self.calculate_dAtomicEnergy_dParameters(afp=afp, index=index, symbol=symbol) if denergy_dparameters is None: denergy_dparameters = temp else: denergy_dparameters += temp return denergy_dparameters def calculate_numerical_dEnergy_dParameters(self, fingerprints, d=0.00001): """Evaluates dEnergy_dParameters using finite difference. This will trigger two calls to calculate_energy(), with each parameter perturbed plus/minus d. Parameters ---------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. d : float The amount of perturbation in each parameter. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': vector = self.vector denergy_dparameters = [] for _ in range(len(vector)): vector[_] += d self.vector = vector eplus = self.calculate_energy(fingerprints) vector[_] -= 2 * d self.vector = vector eminus = self.calculate_energy(fingerprints) denergy_dparameters += [(eplus - eminus) / (2 * d)] vector[_] += d self.vector = vector denergy_dparameters = np.array(denergy_dparameters) return denergy_dparameters def calculate_dForces_dParameters(self, fingerprints, fingerprintprimes): """Calculates an array of floats corresponding to the derivative of model-predicted atomic forces of an image with respect to model parameters. Parameters ---------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. fingerprintprimes : dict Dictionary with images hashs as keys and the corresponding fingerprint derivatives as values. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': selfindices = set([key[0] for key in fingerprintprimes.keys()]) dforces_dparameters = {(selfindex, i): None for selfindex in selfindices for i in range(3)} for key in fingerprintprimes.keys(): selfindex, selfsymbol, nindex, nsymbol, i = key derafp = fingerprintprimes[key] afp = fingerprints[nindex][1] temp = self.calculate_dForce_dParameters(afp=afp, derafp=derafp, direction=i, nindex=nindex, nsymbol=nsymbol,) if dforces_dparameters[(selfindex, i)] is None: dforces_dparameters[(selfindex, i)] = temp else: dforces_dparameters[(selfindex, i)] += temp return dforces_dparameters def calculate_numerical_dForces_dParameters(self, fingerprints, fingerprintprimes, d=0.00001): """Evaluates dForces_dParameters using finite difference. This will trigger two calls to calculate_forces(), with each parameter perturbed plus/minus d. Parameters --------- fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. fingerprintprimes : dict Dictionary with images hashs as keys and the corresponding fingerprint derivatives as values. d : float The amount of perturbation in each parameter. """ if self.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif self.parameters.mode == 'atom-centered': selfindices = set([key[0] for key in fingerprintprimes.keys()]) dforces_dparameters = {(selfindex, i): [] for selfindex in selfindices for i in range(3)} vector = self.vector for _ in range(len(vector)): vector[_] += d self.vector = vector fplus = self.calculate_forces(fingerprints, fingerprintprimes) vector[_] -= 2 * d self.vector = vector fminus = self.calculate_forces(fingerprints, fingerprintprimes) for selfindex in selfindices: for i in range(3): dforces_dparameters[(selfindex, i)] += \ [(fplus[selfindex][i] - fminus[selfindex][i]) / ( 2 * d)] vector[_] += d self.vector = vector for selfindex in selfindices: for i in range(3): dforces_dparameters[(selfindex, i)] = \ np.array(dforces_dparameters[(selfindex, i)]) return dforces_dparameters class LossFunction: """Basic loss function, which can be used by the model.get_loss method which is required in standard model classes. This version is pure python and thus will be slow compared to a fortran/parallel implementation. If parallel is None, it will pull it from the model itself. Only use this keyword to override the model's specification. Also has parallelization methods built in. See self.default_parameters for the default values of parameters specified as None. Parameters ---------- energy_coefficient : float Coefficient of the energy contribution in the loss function. force_coefficient : float Coefficient of the force contribution in the loss function. Can set to None as shortcut to turn off force training. convergence : dict Dictionary of keys and values defining convergence. Keys are 'energy_rmse', 'energy_maxresid', 'force_rmse', and 'force_maxresid'. If 'force_rmse' and 'force_maxresid' are both set to None, force training is turned off and force_coefficient is set to None. parallel : dict Parallel configuration dictionary. Will pull from model itself if not specified. overfit : float Multiplier of the weights norm penalty term in the loss function. raise_ConvergenceOccurred : bool If True will raise convergence notice. log_losses : bool If True will log the loss function value in the log file else will not. d : None or float If d is None, both loss function and its gradient are calculated analytically. If d is a float, then gradient of the loss function is calculated by perturbing each parameter plus/minus d. """ default_parameters = {'convergence': {'energy_rmse': 0.001, 'energy_maxresid': None, 'force_rmse': None, 'force_maxresid': None, } } def __init__(self, energy_coefficient=1.0, force_coefficient=0.04, convergence=None, parallel=None, overfit=0., raise_ConvergenceOccurred=True, log_losses=True, d=None): p = self.parameters = Parameters( {'importname': '.model.LossFunction'}) # 'dict' creates a copy; otherwise mutable in class. c = p['convergence'] = dict(self.default_parameters['convergence']) if convergence is not None: for key, value in convergence.items(): p['convergence'][key] = value p['energy_coefficient'] = energy_coefficient p['force_coefficient'] = force_coefficient p['overfit'] = overfit self.raise_ConvergenceOccurred = raise_ConvergenceOccurred self.log_losses = log_losses self.d = d self._step = 0 self._initialized = False self._data_sent = False self._parallel = parallel if (c['force_rmse'] is None) and (c['force_maxresid'] is None): p['force_coefficient'] = None if p['force_coefficient'] is None: c['force_rmse'] = None c['force_maxresid'] = None def attach_model(self, model, fingerprints=None, fingerprintprimes=None, images=None): """Attach the model to be used to the loss function. fingerprints and training images need not be supplied if they are already attached to the model via model.trainingparameters. Parameters ---------- model : object Class representing the regression model. fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. fingerprintprimes : dict Dictionary with images hashs as keys and the corresponding fingerprint derivatives as values. images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. """ self._model = model self.fingerprints = fingerprints self.fingerprintprimes = fingerprintprimes self.images = images def _initialize(self): """Procedures to be run on the first call only, such as establishing SSH sessions, etc.""" if self._initialized is True: return if self._parallel is None: self._parallel = self._model._parallel log = self._model.log if self.fingerprints is None: self.fingerprints = \ self._model.trainingparameters.descriptor.fingerprints # May also make sense to decide whether or not to calculate # fingerprintprimes based on the value of train_forces. if ((self.parameters.force_coefficient is not None) and (self.fingerprintprimes is None)): self.fingerprintprimes = \ self._model.trainingparameters.descriptor.fingerprintprimes if self.images is None: self.images = self._model.trainingparameters.trainingimages if self._parallel['cores'] != 1: # Initialize workers. python = sys.executable workercommand = '%s -m %s' % (python, self.__module__) server, connections, n_pids = setup_parallel(self._parallel, workercommand, log) self._sessions = {'master': server, 'connections': connections, # SSH's/nodes 'n_pids': n_pids} # total no. of workers if self.log_losses: p = self.parameters convergence = p['convergence'] log(' Loss function convergence criteria:') log(' energy_rmse: ' + str(convergence['energy_rmse'])) log(' energy_maxresid: ' + str(convergence['energy_maxresid'])) log(' force_rmse: ' + str(convergence['force_rmse'])) log(' force_maxresid: ' + str(convergence['force_maxresid'])) log(' Loss function set-up:') log(' energy_coefficient: ' + str(p.energy_coefficient)) log(' force_coefficient: ' + str(p.force_coefficient)) log(' overfit: ' + str(p.overfit)) log('\n') if p.force_coefficient is None: header = '%5s %19s %12s %12s %12s' log(header % ('', '', '', '', 'Energy')) log(header % ('Step', 'Time', 'Loss (SSD)', 'EnergyRMSE', 'MaxResid')) log(header % ('=' * 5, '=' * 19, '=' * 12, '=' * 12, '=' * 12)) else: header = '%5s %19s %12s %12s %12s %12s %12s' log(header % ('', '', '', '', 'Energy', '', 'Force')) log(header % ('Step', 'Time', 'Loss (SSD)', 'EnergyRMSE', 'MaxResid', 'ForceRMSE', 'MaxResid')) log(header % ('=' * 5, '=' * 19, '=' * 12, '=' * 12, '=' * 12, '=' * 12, '=' * 12)) self._initialized = True def _send_data_to_fortran(self,): """Procedures to be run in fortran mode for a single requested core only. Also just on the first call for sending data to fortran modules. """ if self._data_sent is True: return num_images = len(self.images) p = self.parameters energy_coefficient = p.energy_coefficient overfit = p.overfit if p.force_coefficient is None: train_forces = False force_coefficient = 0. else: train_forces = True force_coefficient = p.force_coefficient mode = self._model.parameters.mode if mode == 'atom-centered': num_atoms = None elif mode == 'image-centered': raise NotImplementedError('Image-centered mode is not coded yet.') (actual_energies, actual_forces, elements, atomic_positions, num_images_atoms, atomic_numbers, raveled_fingerprints, num_neighbors, raveled_neighborlists, raveled_fingerprintprimes) = (None,) * 10 value = ravel_data(train_forces, mode, self.images, self.fingerprints, self.fingerprintprimes,) if mode == 'image-centered': if not train_forces: (actual_energies, atomic_positions) = value else: (actual_energies, actual_forces, atomic_positions) = value else: if not train_forces: (actual_energies, elements, num_images_atoms, atomic_numbers, raveled_fingerprints) = value else: (actual_energies, actual_forces, elements, num_images_atoms, atomic_numbers, raveled_fingerprints, num_neighbors, raveled_neighborlists, raveled_fingerprintprimes) = value send_data_to_fortran(fmodules, energy_coefficient, force_coefficient, overfit, train_forces, num_atoms, num_images, actual_energies, actual_forces, atomic_positions, num_images_atoms, atomic_numbers, raveled_fingerprints, num_neighbors, raveled_neighborlists, raveled_fingerprintprimes, self._model, self.d) self._data_sent = True def _cleanup(self): """Closes SSH sessions.""" self._initialized = False if not hasattr(self, '_sessions'): return server = self._sessions['master'] finished = np.array([False] * self._sessions['n_pids']) while not finished.all(): message = server.recv_pyobj() if (message['subject'] == '' and message['data'] == 'parameters'): server.send_pyobj('') finished[int(message['id'])] = True for _ in self._sessions['connections']: if hasattr(_, 'logout'): _.logout() del self._sessions['connections'] def get_loss(self, parametervector, lossprime): """Returns the current value of the loss function for a given set of parameters, or, if the energy is less than the energy_tol raises a ConvergenceException. Parameters ---------- parametervector : list Parameters of the regression model in the form of a list. lossprime : bool If True, will calculate and return dloss_dparameters, else will only return zero for dloss_dparameters. """ self._initialize() if self._parallel['cores'] == 1: if self._model.fortran: self._model.vector = parametervector self._send_data_to_fortran() (loss, dloss_dparameters, energy_loss, force_loss, energy_maxresid, force_maxresid) = \ fmodules.calculate_loss(parameters=parametervector, num_parameters=len( parametervector), lossprime=lossprime) else: loss, dloss_dparameters, energy_loss, force_loss, \ energy_maxresid, force_maxresid = \ self.calculate_loss(parametervector, lossprime=lossprime) else: server = self._sessions['master'] n_pids = self._sessions['n_pids'] # Subdivide tasks. keys = make_sublists(self.images.keys(), n_pids) args = {'lossprime': lossprime, 'd': self.d} results = self.process_parallels(parametervector, server, n_pids, keys, args=args) loss = results['loss'] dloss_dparameters = results['dloss_dparameters'] energy_loss = results['energy_loss'] force_loss = results['force_loss'] energy_maxresid = results['energy_maxresid'] force_maxresid = results['force_maxresid'] self.loss, self.energy_loss, self.force_loss, \ self.energy_maxresid, self.force_maxresid = \ loss, energy_loss, force_loss, energy_maxresid, force_maxresid if lossprime: self.dloss_dparameters = dloss_dparameters if self.raise_ConvergenceOccurred: # Only during calculation of loss function (and not lossprime) # convergence is checked and values are printed out in the log # file. if lossprime is False: self._model.vector = parametervector converged = self.check_convergence(loss, energy_loss, force_loss, energy_maxresid, force_maxresid) if converged: self._cleanup() if self._parallel['cores'] != 1: # Needed to properly close socket connection # (python3). server.close() raise ConvergenceOccurred() return {'loss': self.loss, 'dloss_dparameters': (self.dloss_dparameters if lossprime is True else dloss_dparameters), 'energy_loss': self.energy_loss, 'force_loss': self.force_loss, 'energy_maxresid': self.energy_maxresid, 'force_maxresid': self.force_maxresid, } def calculate_loss(self, parametervector, lossprime): """Method that calculates the loss, derivative of the loss with respect to parameters (if requested), and max_residual. Parameters ---------- parametervector : list Parameters of the regression model in the form of a list. lossprime : bool If True, will calculate and return dloss_dparameters, else will only return zero for dloss_dparameters. """ self._model.vector = parametervector p = self.parameters energyloss = 0. forceloss = 0. energy_maxresid = 0. force_maxresid = 0. dloss_dparameters = np.array([0.] * len(parametervector)) model = self._model for hash in self.images.keys(): image = self.images[hash] no_of_atoms = len(image) amp_energy = model.calculate_energy(self.fingerprints[hash]) actual_energy = image.get_potential_energy(apply_constraint=False) residual_per_atom = abs(amp_energy - actual_energy) / \ len(image) if residual_per_atom > energy_maxresid: energy_maxresid = residual_per_atom energyloss += residual_per_atom**2 # Calculates derivative of the loss function with respect to # parameters if lossprime is true if lossprime: if model.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif model.parameters.mode == 'atom-centered': if self.d is None: denergy_dparameters = \ model.calculate_dEnergy_dParameters( self.fingerprints[hash]) else: denergy_dparameters = \ model.calculate_numerical_dEnergy_dParameters( self.fingerprints[hash], d=self.d) temp = p.energy_coefficient * 2. * \ (amp_energy - actual_energy) * \ denergy_dparameters / \ (no_of_atoms ** 2.) dloss_dparameters += temp if p.force_coefficient is not None: amp_forces = \ model.calculate_forces(self.fingerprints[hash], self.fingerprintprimes[hash]) actual_forces = image.get_forces(apply_constraint=False) for index in range(no_of_atoms): for i in range(3): force_resid = abs(amp_forces[index][i] - actual_forces[index][i]) if force_resid > force_maxresid: force_maxresid = force_resid temp = (1. / 3.) * (amp_forces[index][i] - actual_forces[index][i]) ** 2. / \ no_of_atoms forceloss += temp # Calculates derivative of the loss function with respect to # parameters if lossprime is true if lossprime: if model.parameters.mode == 'image-centered': raise NotImplementedError('This needs to be coded.') elif model.parameters.mode == 'atom-centered': if self.d is None: dforces_dparameters = \ model.calculate_dForces_dParameters( self.fingerprints[hash], self.fingerprintprimes[hash]) else: dforces_dparameters = \ model.calculate_numerical_dForces_dParameters( self.fingerprints[hash], self.fingerprintprimes[hash], d=self.d) for selfindex in range(no_of_atoms): for i in range(3): temp = p.force_coefficient * (2.0 / 3.0) * \ (amp_forces[selfindex][i] - actual_forces[selfindex][i]) * \ dforces_dparameters[(selfindex, i)] \ / no_of_atoms dloss_dparameters += temp loss = p.energy_coefficient * energyloss if p.force_coefficient is not None: loss += p.force_coefficient * forceloss dloss_dparameters = np.array(dloss_dparameters) # if overfit coefficient is more than zero, overfit contribution to # loss and dloss_dparameters is also added. if p.overfit > 0.: overfitloss = 0. for component in parametervector: overfitloss += component ** 2. overfitloss *= p.overfit loss += overfitloss doverfitloss_dparameters = \ 2 * p.overfit * np.array(parametervector) dloss_dparameters += doverfitloss_dparameters return loss, dloss_dparameters, energyloss, forceloss, \ energy_maxresid, force_maxresid # All incoming requests will be dictionaries with three keys. # d['id']: process id number, assigned when process created above. # d['subject']: what the message is asking for / telling you. # d['data']: optional data passed from worker. def process_parallels(self, vector, server, n_pids, keys, args): """ Parameters ---------- vector : list Parameters of the regression model in the form of a list. server : object Master session of parallel processing. processes: list of objects Worker sessions for parallel processing. keys : list List of images keys for worker processes. args : dict Dictionary containing arguments of the method to be called on each worker process. """ # For each process finished = np.array([False] * n_pids) results = {'loss': 0., 'dloss_dparameters': [0.] * len(vector), 'energy_loss': 0., 'force_loss': 0., 'energy_maxresid': 0., 'force_maxresid': 0.} while not finished.all(): message = server.recv_pyobj() if message['subject'] == '': server.send_string('calculate_loss_function') elif message['subject'] == '': request = message['data'] # Variable name. if request == 'images': subimages = {k: self.images[k] for k in keys[int(message['id'])]} server.send_pyobj(subimages) elif request == 'fortran': server.send_pyobj(self._model.fortran) elif request == 'modelstring': server.send_pyobj(self._model.tostring()) elif request == 'lossfunctionstring': server.send_pyobj(self.parameters.tostring()) elif request == 'fingerprints': server.send_pyobj({k: self.fingerprints[k] for k in keys[int(message['id'])]}) elif request == 'fingerprintprimes': if self.fingerprintprimes is not None: server.send_pyobj({k: self.fingerprintprimes[k] for k in keys[int(message['id'])]}) else: server.send_pyobj(None) elif request == 'args': server.send_pyobj(args) elif request == 'parameters': if finished[int(message['id'])]: server.send_pyobj('') else: server.send_pyobj(vector) else: raise NotImplementedError() elif message['subject'] == '': result = message['data'] server.send_string('meaningless reply') results['loss'] += result['loss'] results['dloss_dparameters'] += result['dloss_dparameters'] results['energy_loss'] += result['energy_loss'] results['force_loss'] += result['force_loss'] if result['energy_maxresid'] > results['energy_maxresid']: results['energy_maxresid'] = result['energy_maxresid'] if result['force_maxresid'] > results['force_maxresid']: results['force_maxresid'] = result['force_maxresid'] finished[int(message['id'])] = True return results def check_convergence(self, loss, energy_loss, force_loss, energy_maxresid, force_maxresid): """Check convergence Checks to see whether convergence is met; if it is, raises ConvergenceException to stop the optimizer. Parameters ---------- loss : float Value of the loss function. energy_loss : float Value of the energy contribution of the loss function. force_loss : float Value of the force contribution of the loss function. energy_maxresid : float Maximum energy residual. force_maxresid : float Maximum force residual. """ p = self.parameters energy_rmse_converged = True log = self._model.log if p.convergence['energy_rmse'] is not None: energy_rmse = np.sqrt(energy_loss / len(self.images)) if energy_rmse > p.convergence['energy_rmse']: energy_rmse_converged = False energy_maxresid_converged = True if p.convergence['energy_maxresid'] is not None: if energy_maxresid > p.convergence['energy_maxresid']: energy_maxresid_converged = False if p.force_coefficient is not None: force_rmse_converged = True if p.convergence['force_rmse'] is not None: force_rmse = np.sqrt(force_loss / len(self.images)) if force_rmse > p.convergence['force_rmse']: force_rmse_converged = False force_maxresid_converged = True if p.convergence['force_maxresid'] is not None: if force_maxresid > p.convergence['force_maxresid']: force_maxresid_converged = False if self.log_losses: log('%5i %19s %12.4e %10.4e %1s' ' %10.4e %1s %10.4e %1s %10.4e %1s' % (self._step, now(), loss, energy_rmse, 'C' if energy_rmse_converged else '-', energy_maxresid, 'C' if energy_maxresid_converged else '-', force_rmse, 'C' if force_rmse_converged else '-', force_maxresid, 'C' if force_maxresid_converged else '-')) self._step += 1 return energy_rmse_converged and energy_maxresid_converged and \ force_rmse_converged and force_maxresid_converged else: if self.log_losses: log('%5i %19s %12.4e %10.4e %1s %10.4e %1s' % (self._step, now(), loss, energy_rmse, 'C' if energy_rmse_converged else '-', energy_maxresid, 'C' if energy_maxresid_converged else '-')) self._step += 1 return energy_rmse_converged and energy_maxresid_converged def calculate_fingerprints_range(fp, images): """Calculates the range for the fingerprints corresponding to images, stored in fp. fp is a fingerprints object with the fingerprints data stored in a dictionary-like object at fp.fingerprints. (Typically this is a .utilties.Data structure.) images is a hashed dictionary of atoms for which to consider the range. In image-centered mode, returns an array of (min, max) values for each fingerprint. In atom-centered mode, returns a dictionary of such arrays, one per element. """ if fp.parameters.mode == 'image-centered': raise NotImplementedError() elif fp.parameters.mode == 'atom-centered': fprange = {} for hash in images.keys(): imagefingerprints = fp.fingerprints[hash] for element, fingerprint in imagefingerprints: if element not in fprange: fprange[element] = [[_, _] for _ in fingerprint] else: assert len(fprange[element]) == len(fingerprint) for i, ridge in enumerate(fingerprint): if ridge < fprange[element][i][0]: fprange[element][i][0] = ridge elif ridge > fprange[element][i][1]: fprange[element][i][1] = ridge for key, value in fprange.items(): fprange[key] = value return fprange def ravel_data(train_forces, mode, images, fingerprints, fingerprintprimes,): """ Reshapes data of images into lists. Parameters --------- train_forces : bool Determining whether forces are also trained or not. mode : str Can be either 'atom-centered' or 'image-centered'. images : list or str List of ASE atoms objects with positions, symbols, energies, and forces in ASE format. This is the training set of data. This can also be the path to an ASE trajectory (.traj) or database (.db) file. Energies can be obtained from any reference, e.g. DFT calculations. fingerprints : dict Dictionary with images hashs as keys and the corresponding fingerprints as values. fingerprintprimes : dict Dictionary with images hashs as keys and the corresponding fingerprint derivatives as values. """ from ase.data import atomic_numbers actual_energies = [image.get_potential_energy(apply_constraint=False) for image in images.values()] if mode == 'atom-centered': num_images_atoms = [len(image) for image in images.values()] atomic_numbers = [atomic_numbers[atom.symbol] for image in images.values() for atom in image] def ravel_fingerprints(images, fingerprints): """ Reshape fingerprints of images into a list. """ raveled_fingerprints = [] elements = [] for hash, image in images.items(): for index in range(len(image)): elements += [fingerprints[hash][index][0]] raveled_fingerprints += [fingerprints[hash][index][1]] elements = sorted(set(elements)) # Could also work without images: # raveled_fingerprints = [afp # for hash, value in fingerprints.iteritems() # for (element, afp) in value] return elements, raveled_fingerprints elements, raveled_fingerprints = ravel_fingerprints(images, fingerprints) else: atomic_positions = [image.positions.ravel() for image in images.values()] if train_forces is True: actual_forces = \ [image.get_forces(apply_constraint=False)[index] for image in images.values() for index in range(len(image))] if mode == 'atom-centered': def ravel_neighborlists_and_fingerprintprimes(images, fingerprintprimes): """ Reshape neighborlists and fingerprintprimes of images into a list and a matrix, respectively. """ # Only neighboring atoms of type II (within the main cell) # need to be sent to fortran for force training. # All keys in fingerprintprimes are for type II neighborhoods. # Also note that each atom is considered as neighbor of # itself in fingerprintprimes. num_neighbors = [] raveled_neighborlists = [] raveled_fingerprintprimes = [] for hash, image in images.items(): for atom in image: selfindex = atom.index selfsymbol = atom.symbol selfneighborindices = [] selfneighborsymbols = [] for key, derafp in fingerprintprimes[hash].items(): # key = (selfindex, selfsymbol, nindex, nsymbol, i) # i runs from 0 to 2. neighbor indices and symbols # should be added just once. if key[0] == selfindex and key[4] == 0: selfneighborindices += [key[2]] selfneighborsymbols += [key[3]] neighborcount = 0 for nindex, nsymbol in zip(selfneighborindices, selfneighborsymbols): raveled_neighborlists += [nindex] neighborcount += 1 for i in range(3): fpprime = fingerprintprimes[hash][(selfindex, selfsymbol, nindex, nsymbol, i)] raveled_fingerprintprimes += [fpprime] num_neighbors += [neighborcount] return (num_neighbors, raveled_neighborlists, raveled_fingerprintprimes) (num_neighbors, raveled_neighborlists, raveled_fingerprintprimes) = \ ravel_neighborlists_and_fingerprintprimes(images, fingerprintprimes) if mode == 'image-centered': if not train_forces: return (actual_energies, atomic_positions) else: return (actual_energies, actual_forces, atomic_positions) else: if not train_forces: return (actual_energies, elements, num_images_atoms, atomic_numbers, raveled_fingerprints) else: return (actual_energies, actual_forces, elements, num_images_atoms, atomic_numbers, raveled_fingerprints, num_neighbors, raveled_neighborlists, raveled_fingerprintprimes) def send_data_to_fortran(_fmodules, energy_coefficient, force_coefficient, overfit, train_forces, num_atoms, num_images, actual_energies, actual_forces, atomic_positions, num_images_atoms, atomic_numbers, raveled_fingerprints, num_neighbors, raveled_neighborlists, raveled_fingerprintprimes, model, d): """ Function that sends images data to fortran code. Is used just once on each core. """ from ase.data import atomic_numbers as an if model.parameters.mode == 'image-centered': mode_signal = 1 elif model.parameters.mode == 'atom-centered': mode_signal = 2 _fmodules.images_props.num_images = num_images _fmodules.images_props.actual_energies = actual_energies if train_forces: _fmodules.images_props.actual_forces = actual_forces _fmodules.model_props.energy_coefficient = energy_coefficient _fmodules.model_props.force_coefficient = force_coefficient _fmodules.model_props.overfit = overfit _fmodules.model_props.train_forces = train_forces _fmodules.model_props.mode_signal = mode_signal if d is None: _fmodules.model_props.numericprime = False else: _fmodules.model_props.numericprime = True _fmodules.model_props.d = d if model.parameters.mode == 'atom-centered': fprange = model.parameters.fprange elements = sorted(fprange.keys()) num_elements = len(elements) elements_numbers = [an[elm] for elm in elements] min_fingerprints = \ [[fprange[elm][_][0] for _ in range(len(fprange[elm]))] for elm in elements] max_fingerprints = [[fprange[elm][_][1] for _ in range(len(fprange[elm]))] for elm in elements] num_fingerprints_of_elements = \ [len(fprange[elm]) for elm in elements] _fmodules.images_props.num_elements = num_elements _fmodules.images_props.elements_numbers = elements_numbers _fmodules.images_props.num_images_atoms = num_images_atoms _fmodules.images_props.atomic_numbers = atomic_numbers if train_forces: _fmodules.images_props.num_neighbors = num_neighbors _fmodules.images_props.raveled_neighborlists = \ raveled_neighborlists _fmodules.fingerprint_props.num_fingerprints_of_elements = \ num_fingerprints_of_elements _fmodules.fingerprint_props.raveled_fingerprints = raveled_fingerprints _fmodules.neuralnetwork.min_fingerprints = min_fingerprints _fmodules.neuralnetwork.max_fingerprints = max_fingerprints if train_forces: _fmodules.fingerprint_props.raveled_fingerprintprimes = \ raveled_fingerprintprimes else: _fmodules.images_props.num_atoms = num_atoms _fmodules.images_props.atomic_positions = atomic_positions # for neural neyworks only if model.parameters['importname'] == '.model.neuralnetwork.NeuralNetwork': hiddenlayers = model.parameters.hiddenlayers activation = model.parameters.activation if model.parameters.mode == 'atom-centered': from collections import OrderedDict no_layers_of_elements = \ [3 if isinstance(hiddenlayers[elm], int) else (len(hiddenlayers[elm]) + 2) for elm in elements] nn_structure = OrderedDict() for elm in elements: len_of_fps = len(fprange[elm]) if isinstance(hiddenlayers[elm], int): nn_structure[elm] = \ ([len_of_fps] + [hiddenlayers[elm]] + [1]) else: nn_structure[elm] = \ ([len_of_fps] + [layer for layer in hiddenlayers[elm]] + [1]) no_nodes_of_elements = [nn_structure[elm][_] for elm in elements for _ in range(len(nn_structure[elm]))] else: num_atoms = model.parameters.num_atoms if isinstance(hiddenlayers, int): no_layers_of_elements = [3] else: no_layers_of_elements = [len(hiddenlayers) + 2] if isinstance(hiddenlayers, int): nn_structure = ([3 * num_atoms] + [hiddenlayers] + [1]) else: nn_structure = ([3 * num_atoms] + [layer for layer in hiddenlayers] + [1]) no_nodes_of_elements = [nn_structure[_] for _ in range(len(nn_structure))] _fmodules.neuralnetwork.no_layers_of_elements = no_layers_of_elements _fmodules.neuralnetwork.no_nodes_of_elements = no_nodes_of_elements if activation == 'tanh': activation_signal = 1 elif activation == 'sigmoid': activation_signal = 2 elif activation == 'linear': activation_signal = 3 _fmodules.neuralnetwork.activation_signal = activation_signal andrewpeterson-amp-4878fc892f2c/amp/model/__main__.py000066400000000000000000000103241332417112400224150ustar00rootroot00000000000000"""Directly calling this module; apparently from another node. Calls should come as python -m amp.model id hostname:port This session will then start a zmq session with that socket, labeling itself with id. Instructions on what to do will come from the socket. """ import sys import tempfile import zmq from ..utilities import MessageDictionary, string2dict, Logger from .. import importhelper hostsocket = sys.argv[-1] proc_id = sys.argv[-2] msg = MessageDictionary(proc_id) # Send standard lines to stdout signaling process started and where # error is directed. print('') # Signal that program started. sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('Log and stderr written to %s' % sys.stderr.name) # Also send logger output to stderr to aid in debugging. log = Logger(file=sys.stderr) # Establish client session via zmq; find purpose. context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect('tcp://%s' % hostsocket) socket.send_pyobj(msg('')) purpose = socket.recv_string() if purpose == 'calculate_loss_function': # Request variables. socket.send_pyobj(msg('', 'fortran')) fortran = socket.recv_pyobj() socket.send_pyobj(msg('', 'modelstring')) modelstring = socket.recv_pyobj() dictionary = string2dict(modelstring) Model = importhelper(dictionary.pop('importname')) log('Model received:') log(str(dictionary)) model = Model(fortran=fortran, **dictionary) model.log = log log('Model set up.') socket.send_pyobj(msg('', 'args')) args = socket.recv_pyobj() d = args['d'] socket.send_pyobj(msg('', 'lossfunctionstring')) lossfunctionstring = socket.recv_pyobj() dictionary = string2dict(lossfunctionstring) log(str(dictionary)) LossFunction = importhelper(dictionary.pop('importname')) lossfunction = LossFunction(parallel={'cores': 1}, raise_ConvergenceOccurred=False, d=d, **dictionary) log('Loss function set up.') images = None socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() log('Images received.') fingerprints = None socket.send_pyobj(msg('', 'fingerprints')) fingerprints = socket.recv_pyobj() log('Fingerprints received.') fingerprintprimes = None socket.send_pyobj(msg('', 'fingerprintprimes')) fingerprintprimes = socket.recv_pyobj() log('Fingerprintprimes received.') # Set up local loss function. lossfunction.attach_model(model, fingerprints=fingerprints, fingerprintprimes=fingerprintprimes, images=images) log('Images, fingerprints, and fingerprintprimes ' 'attached to the loss function.') if model.fortran: log('fmodules will be used to evaluate loss function.') else: log('Fortran will not be used to evaluate loss function.') # Now wait for parameters, and send the component of the loss function. while True: socket.send_pyobj(msg('', 'parameters')) parameters = socket.recv_pyobj() if parameters == '': # FIXME/ap: I removed an fmodules.deallocate_variables() call # here. Do we need to add this to LossFunction? break elif parameters == '': # Master is waiting for other workers to finish. # Any more elegant way # to do this without opening another comm channel? # or having a thread for each process? pass else: # FIXME/ap: Why do we need to request this every time? # Couldn't it be part of earlier request? socket.send_pyobj(msg('', 'args')) args = socket.recv_pyobj() lossprime = args['lossprime'] output = lossfunction.get_loss(parameters, lossprime=lossprime) socket.send_pyobj(msg('', output)) socket.recv_string() else: raise NotImplementedError('Purpose "%s" unknown.' % purpose) andrewpeterson-amp-4878fc892f2c/amp/model/neuralnetwork.f90000066400000000000000000002176261332417112400235610ustar00rootroot00000000000000!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! module that utilizes the regression model to calculate energies ! and forces as well as their derivatives. Function names ending ! with an underscore correspond to image-centered mode. module neuralnetwork implicit none ! the data of neuralnetwork (should be fed in by python) double precision, allocatable::min_fingerprints(:, :) double precision, allocatable::max_fingerprints(:, :) integer, allocatable:: no_layers_of_elements(:) integer, allocatable:: no_nodes_of_elements(:) integer:: activation_signal type:: real_two_d_array sequence double precision, allocatable:: twodarray(:,:) end type real_two_d_array type:: element_parameters sequence double precision:: intercept double precision:: slope type(real_two_d_array), allocatable:: weights(:) end type element_parameters type:: real_one_d_array sequence double precision, allocatable:: onedarray(:) end type real_one_d_array contains !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns energy value in the image-centered mode. function calculate_image_energy(num_inputs, inputs, num_parameters, & parameters) implicit none integer:: num_inputs, num_parameters double precision:: inputs(num_inputs) double precision:: parameters(num_parameters) double precision:: calculate_image_energy integer:: p, m, n, layer integer:: l, j, num_rows, num_cols, q integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_two_d_array), allocatable:: weights(:) double precision:: intercept double precision:: slope ! changing the form of parameters from vector into derived-types l = 0 allocate(weights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols weights(j)%twodarray(p, q) = & parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do intercept = parameters(l + 1) slope = parameters(l + 2) allocate(hiddensizes(no_layers_of_elements(1) - 2)) do m = 1, no_layers_of_elements(1) - 2 hiddensizes(m) = no_nodes_of_elements(m + 1) end do allocate(o(no_layers_of_elements(1))) allocate(ohat(no_layers_of_elements(1))) layer = 1 allocate(o(1)%onedarray(num_inputs)) allocate(ohat(1)%onedarray(num_inputs + 1)) do m = 1, num_inputs o(1)%onedarray(m) = inputs(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(weights(layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(& size(weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(weights(layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(weights(layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) & * weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = & tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do calculate_image_energy = slope * o(layer)%onedarray(1) + intercept ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) ! deallocating derived type parameters do p = 1, size(weights) deallocate(weights(p)%twodarray) end do deallocate(weights) end function calculate_image_energy !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns energy value in the atom-centered mode. function calculate_atomic_energy(symbol, & len_of_fingerprint, fingerprint, & num_elements, elements_numbers, & num_parameters, parameters) implicit none integer:: symbol, num_parameters, & len_of_fingerprint, num_elements double precision:: fingerprint(len_of_fingerprint) integer:: elements_numbers(num_elements) double precision:: parameters(num_parameters) double precision:: calculate_atomic_energy integer:: p, element, m, n, layer integer:: k, l, j, num_rows, num_cols, q integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(element_parameters):: unraveled_parameters(num_elements) double precision:: fingerprint_(len_of_fingerprint) ! scaling fingerprints do element = 1, num_elements if (symbol == & elements_numbers(element)) then exit end if end do do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprint_(l) = -1.0d0 + 2.0d0 * & (fingerprint(l) - min_fingerprints(element, l)) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprint_(l) = fingerprint(l) endif end do ! changing the form of parameters from vector into derived-types k = 0 l = 0 do element = 1, num_elements allocate(unraveled_parameters(element)%weights(& no_layers_of_elements(element)-1)) if (element .GT. 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_parameters(& element)%weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_parameters(element)%weights(j)%twodarray(& p, q) = parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements unraveled_parameters(element)%intercept = & parameters(l + 2 * element - 1) unraveled_parameters(element)%slope = & parameters(l + 2 * element) end do p = 0 do element = 1, num_elements if (symbol == elements_numbers(element)) then exit else p = p + no_layers_of_elements(element) end if end do allocate(hiddensizes(no_layers_of_elements(element) - 2)) do m = 1, no_layers_of_elements(element) - 2 hiddensizes(m) = no_nodes_of_elements(p + m + 1) end do allocate(o(no_layers_of_elements(element))) allocate(ohat(no_layers_of_elements(element))) layer = 1 allocate(o(1)%onedarray(len_of_fingerprint)) allocate(ohat(1)%onedarray(len_of_fingerprint + 1)) do m = 1, len_of_fingerprint o(1)%onedarray(m) = fingerprint_(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * unraveled_parameters(& element)%weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do calculate_atomic_energy = unraveled_parameters(element)%slope * & o(layer)%onedarray(1) + unraveled_parameters(element)%intercept ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) ! deallocating derived type parameters do element = 1, num_elements deallocate(unraveled_parameters(element)%weights) end do end function calculate_atomic_energy !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns force value in the image-centered mode. function calculate_force_(num_inputs, inputs, inputs_, & num_parameters, parameters) implicit none integer:: num_inputs, num_parameters double precision:: inputs(num_inputs) double precision:: inputs_(num_inputs) double precision:: parameters(num_parameters) double precision:: calculate_force_ double precision, allocatable:: temp(:) integer:: p, q, m, n, nn, layer integer:: l, j, num_rows, num_cols integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: doutputs_dinputs(:) type(real_two_d_array), allocatable:: weights(:) double precision:: intercept double precision:: slope ! changing the form of parameters to derived-types l = 0 allocate(weights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols weights(j)%twodarray(p, q) = & parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do intercept = parameters(l + 1) slope = parameters(l + 2) allocate(hiddensizes(no_layers_of_elements(1) - 2)) do m = 1, no_layers_of_elements(1) - 2 hiddensizes(m) = no_nodes_of_elements(m + 1) end do allocate(o(no_layers_of_elements(1))) allocate(ohat(no_layers_of_elements(1))) layer = 1 allocate(o(1)%onedarray(num_inputs)) allocate(ohat(1)%onedarray(num_inputs + 1)) do m = 1, num_inputs o(1)%onedarray(m) = inputs(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(weights(layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(& size(weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(weights(layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(weights(layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * & weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do deallocate(net) end do nn = size(o) - 2 allocate(doutputs_dinputs(nn + 2)) allocate(doutputs_dinputs(1)%onedarray(num_inputs)) do m = 1, num_inputs doutputs_dinputs(1)%onedarray(m) = inputs_(m) end do do layer = 1, nn + 1 allocate(temp(size(weights(layer)%twodarray, dim = 2))) do p = 1, size(weights(layer)%twodarray, dim = 2) temp(p) = 0.0d0 do q = 1, size(weights(layer)%twodarray, dim = 1) - 1 temp(p) = temp(p) + doutputs_dinputs(& layer)%onedarray(q) * weights(layer)%twodarray(q, p) end do end do q = size(o(layer + 1)%onedarray) allocate(doutputs_dinputs(layer + 1)%onedarray(q)) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then doutputs_dinputs(layer + 1)%onedarray(p) = & temp(p) * (1.0d0 - o(layer + 1)%onedarray(p) * & o(layer + 1)%onedarray(p)) else if (activation_signal == 2) then doutputs_dinputs(layer + 1)%onedarray(p) = & temp(p) * (1.0d0 - o(layer + 1)%onedarray(p)) * & o(layer + 1)%onedarray(p) else if (activation_signal == 3) then doutputs_dinputs(layer+ 1)%onedarray(p) = temp(p) end if end do deallocate(temp) end do calculate_force_ = slope * doutputs_dinputs(nn + 2)%onedarray(1) ! force is multiplied by -1, because it is -dE/dx and not dE/dx. calculate_force_ = -1.0d0 * calculate_force_ ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(doutputs_dinputs) deallocate(doutputs_dinputs(p)%onedarray) end do deallocate(doutputs_dinputs) ! deallocating derived type parameters do p = 1, size(weights) deallocate(weights(p)%twodarray) end do deallocate(weights) end function calculate_force_ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns force value in the atom-centered mode. function calculate_force(symbol, len_of_fingerprint, fingerprint, & fingerprintprime, num_elements, elements_numbers, & num_parameters, parameters) implicit none integer:: symbol, len_of_fingerprint, num_parameters integer:: num_elements double precision:: fingerprint(len_of_fingerprint) double precision:: fingerprintprime(len_of_fingerprint) integer:: elements_numbers(num_elements) double precision:: parameters(num_parameters) double precision:: calculate_force double precision, allocatable:: temp(:) integer:: p, q, element, m, n, nn, layer integer:: k, l, j, num_rows, num_cols integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: doutputs_dinputs(:) type(element_parameters):: unraveled_parameters(num_elements) double precision:: fingerprint_(len_of_fingerprint) double precision:: fingerprintprime_(len_of_fingerprint) ! scaling fingerprints do element = 1, num_elements if (symbol == & elements_numbers(element)) then exit end if end do do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprint_(l) = -1.0d0 + 2.0d0 * & (fingerprint(l) - min_fingerprints(element, l)) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprint_(l) = fingerprint(l) endif end do ! scaling fingerprintprimes do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprintprime_(l) = & 2.0d0 * fingerprintprime(l) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprintprime_(l) = fingerprintprime(l) endif end do ! changing the form of parameters to derived-types k = 0 l = 0 do element = 1, num_elements allocate(unraveled_parameters(element)%weights(& no_layers_of_elements(element)-1)) if (element .GT. 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_parameters(& element)%weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_parameters(element)%weights(j)%twodarray(& p, q) = parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements unraveled_parameters(element)%intercept = & parameters(l + 2 * element - 1) unraveled_parameters(element)%slope = & parameters(l + 2 * element) end do p = 0 do element = 1, num_elements if (symbol == elements_numbers(element)) then exit else p = p + no_layers_of_elements(element) end if end do allocate(hiddensizes(no_layers_of_elements(element) - 2)) do m = 1, no_layers_of_elements(element) - 2 hiddensizes(m) = no_nodes_of_elements(p + m + 1) end do allocate(o(no_layers_of_elements(element))) allocate(ohat(no_layers_of_elements(element))) layer = 1 allocate(o(1)%onedarray(len_of_fingerprint)) allocate(ohat(1)%onedarray(len_of_fingerprint + 1)) do m = 1, len_of_fingerprint o(1)%onedarray(m) = fingerprint_(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * unraveled_parameters(& element)%weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do deallocate(net) end do nn = size(o) - 2 allocate(doutputs_dinputs(nn + 2)) allocate(doutputs_dinputs(1)%onedarray(& len_of_fingerprint)) do m = 1, len_of_fingerprint doutputs_dinputs(1)%onedarray(m) = fingerprintprime_(m) end do do layer = 1, nn + 1 allocate(temp(size(unraveled_parameters(element)%weights(& layer)%twodarray, dim = 2))) do p = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim = 2) temp(p) = 0.0d0 do q = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim = 1) - 1 temp(p) = temp(p) + doutputs_dinputs(& layer)%onedarray(q) * unraveled_parameters(& element)%weights(layer)%twodarray(q, p) end do end do q = size(o(layer + 1)%onedarray) allocate(doutputs_dinputs(layer + 1)%onedarray(q)) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then doutputs_dinputs(layer + 1)%onedarray(p) = temp(p) * & (1.0d0 - o(layer + 1)%onedarray(p) * & o(layer + 1)%onedarray(p)) else if (activation_signal == 2) then doutputs_dinputs(layer + 1)%onedarray(p) = & temp(p) * (1.0d0 - o(layer + 1)%onedarray(p)) * & o(layer + 1)%onedarray(p) else if (activation_signal == 3) then doutputs_dinputs(layer+ 1)%onedarray(p) = temp(p) end if end do deallocate(temp) end do calculate_force = unraveled_parameters(element)%slope * & doutputs_dinputs(nn + 2)%onedarray(1) ! force is multiplied by -1, because it is -dE/dx and not dE/dx. calculate_force = -1.0d0 * calculate_force ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(doutputs_dinputs) deallocate(doutputs_dinputs(p)%onedarray) end do deallocate(doutputs_dinputs) ! deallocating derived type parameters do element = 1, num_elements deallocate(unraveled_parameters(element)%weights) end do end function calculate_force !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns derivative of energy w.r.t. parameters in the ! image-centered mode. function calculate_denergy_dparameters_(num_inputs, inputs, & num_parameters, parameters) implicit none integer:: num_inputs, num_parameters double precision:: calculate_denergy_dparameters_(num_parameters) double precision:: parameters(num_parameters) double precision:: inputs(num_inputs) integer:: m, n, j, l, layer, p, q, nn, num_cols, num_rows double precision:: temp1, temp2 integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: delta(:), D(:) type(real_two_d_array), allocatable:: weights(:) double precision:: intercept double precision:: slope type(real_two_d_array), allocatable:: & unraveled_denergy_dweights(:) double precision:: denergy_dintercept double precision:: denergy_dslope ! changing the form of parameters from vector into derived-types l = 0 allocate(weights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols weights(j)%twodarray(p, q) = & parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do intercept = parameters(l + 1) slope = parameters(l + 2) denergy_dintercept = 0.d0 denergy_dslope = 0.d0 l = 0 allocate(unraveled_denergy_dweights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(unraveled_denergy_dweights(j)%twodarray(num_rows, & num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_denergy_dweights(j)%twodarray(p, q) = 0.0d0 end do end do l = l + num_rows * num_cols end do allocate(hiddensizes(no_layers_of_elements(1) - 2)) do m = 1, no_layers_of_elements(1) - 2 hiddensizes(m) = no_nodes_of_elements(m + 1) end do allocate(o(no_layers_of_elements(1))) allocate(ohat(no_layers_of_elements(1))) layer = 1 allocate(o(1)%onedarray(num_inputs)) allocate(ohat(1)%onedarray(num_inputs + 1)) do m = 1, num_inputs o(1)%onedarray(m) = inputs(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(weights(layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(& size(weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(weights(layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(weights(layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * weights(& layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do nn = size(o) - 2 allocate(D(nn + 1)) do layer = 1, nn + 1 allocate(D(layer)%onedarray(size(o(layer + 1)%onedarray))) do j = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then D(layer)%onedarray(j) = & (1.0d0 - o(layer + 1)%onedarray(j)* & o(layer + 1)%onedarray(j)) elseif (activation_signal == 2) then D(layer)%onedarray(j) = o(layer + 1)%onedarray(j) * & (1.0d0 - o(layer + 1)%onedarray(j)) elseif (activation_signal == 3) then D(layer)%onedarray(j) = 1.0d0 end if end do end do allocate(delta(nn + 1)) allocate(delta(nn + 1)%onedarray(1)) delta(nn + 1)%onedarray(1) = D(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(delta(layer)%onedarray(size(D(layer)%onedarray))) do p = 1, size(D(layer)%onedarray) delta(layer)%onedarray(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp1 = D(layer)%onedarray(p) * & weights(layer + 1)%twodarray(p, q) temp2 = temp1 * delta(layer + 1)%onedarray(q) delta(layer)%onedarray(p) = & delta(layer)%onedarray(p) + temp2 end do end do end do denergy_dintercept = 1.0d0 denergy_dslope = o(nn + 2)%onedarray(1) do layer = 1, nn + 1 do p = 1, size(ohat(layer)%onedarray) do q = 1, size(delta(layer)%onedarray) unraveled_denergy_dweights(layer)%twodarray(p, q) = & slope * & ohat(layer)%onedarray(p) * delta(layer)%onedarray(q) end do end do end do deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(delta) deallocate(delta(p)%onedarray) end do deallocate(delta) do p = 1, size(D) deallocate(D(p)%onedarray) end do deallocate(D) ! changing the derivatives of the energy from derived-type ! form into vector l = 0 do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) do p = 1, num_rows do q = 1, num_cols calculate_denergy_dparameters_(& l + (p - 1) * num_cols + q) = & unraveled_denergy_dweights(j)%twodarray(p, q) end do end do l = l + num_rows * num_cols end do calculate_denergy_dparameters_(l + 1) = denergy_dintercept calculate_denergy_dparameters_(l + 2) = denergy_dslope ! deallocating derived-type parameters do p = 1, size(weights) deallocate(weights(p)%twodarray) end do deallocate(weights) do p = 1, size(unraveled_denergy_dweights) deallocate(unraveled_denergy_dweights(p)%twodarray) end do deallocate(unraveled_denergy_dweights) end function calculate_denergy_dparameters_ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns derivative of energy w.r.t. parameters in the ! atom-centered mode. function calculate_datomicenergy_dparameters(symbol, & len_of_fingerprint, fingerprint, num_elements, & elements_numbers, num_parameters, parameters) implicit none integer:: num_parameters, num_elements integer:: symbol, len_of_fingerprint double precision:: calculate_datomicenergy_dparameters(num_parameters) double precision:: parameters(num_parameters) double precision:: fingerprint(len_of_fingerprint) integer:: elements_numbers(num_elements) integer:: element, m, n, j, k, l, layer, p, q, nn, num_cols integer:: num_rows double precision:: temp1, temp2 integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: delta(:), D(:) type(element_parameters):: unraveled_parameters(num_elements) type(element_parameters):: & unraveled_daenergy_dparameters(num_elements) double precision:: fingerprint_(len_of_fingerprint) ! scaling fingerprints do element = 1, num_elements if (symbol == & elements_numbers(element)) then exit end if end do do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprint_(l) = -1.0d0 + 2.0d0 * & (fingerprint(l) - min_fingerprints(element, l)) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprint_(l) = fingerprint(l) endif end do ! changing the form of parameters to derived types k = 0 l = 0 do element = 1, num_elements allocate(unraveled_parameters(element)%weights(& no_layers_of_elements(element)-1)) if (element .GT. 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_parameters(element)%weights(& j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_parameters(element)%weights(j)%twodarray(& p, q) = parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements unraveled_parameters(element)%intercept = & parameters(l + 2 * element - 1) unraveled_parameters(element)%slope = & parameters(l + 2 * element) end do do element = 1, num_elements unraveled_daenergy_dparameters(element)%intercept = 0.d0 unraveled_daenergy_dparameters(element)%slope = 0.d0 end do k = 0 l = 0 do element = 1, num_elements allocate(unraveled_daenergy_dparameters(element)%weights(& no_layers_of_elements(element)-1)) if (element > 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_daenergy_dparameters(& element)%weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_daenergy_dparameters(& element)%weights(j)%twodarray(p, q) = 0.0d0 end do end do l = l + num_rows * num_cols end do end do p = 0 do element = 1, num_elements if (symbol == elements_numbers(element)) then exit else p = p + no_layers_of_elements(element) end if end do allocate(hiddensizes(no_layers_of_elements(element) - 2)) do m = 1, no_layers_of_elements(element) - 2 hiddensizes(m) = no_nodes_of_elements(p + m + 1) end do allocate(o(no_layers_of_elements(element))) allocate(ohat(no_layers_of_elements(element))) layer = 1 allocate(o(1)%onedarray(len_of_fingerprint)) allocate(ohat(1)%onedarray(len_of_fingerprint + 1)) do m = 1, len_of_fingerprint o(1)%onedarray(m) = fingerprint_(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * unraveled_parameters(& element)%weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do nn = size(o) - 2 allocate(D(nn + 1)) do layer = 1, nn + 1 allocate(D(layer)%onedarray(size(o(layer + 1)%onedarray))) do j = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then D(layer)%onedarray(j) = (1.0d0 - & o(layer + 1)%onedarray(j)* o(layer + 1)%onedarray(j)) elseif (activation_signal == 2) then D(layer)%onedarray(j) = o(layer + 1)%onedarray(j) * & (1.0d0 - o(layer + 1)%onedarray(j)) elseif (activation_signal == 3) then D(layer)%onedarray(j) = 1.0d0 end if end do end do allocate(delta(nn + 1)) allocate(delta(nn + 1)%onedarray(1)) delta(nn + 1)%onedarray(1) = D(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(delta(layer)%onedarray(size(D(layer)%onedarray))) do p = 1, size(D(layer)%onedarray) delta(layer)%onedarray(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp1 = D(layer)%onedarray(p) * unraveled_parameters(& element)%weights(layer + 1)%twodarray(p, q) temp2 = temp1 * delta(layer + 1)%onedarray(q) delta(layer)%onedarray(p) = & delta(layer)%onedarray(p) + temp2 end do end do end do unraveled_daenergy_dparameters(element)%intercept = 1.0d0 unraveled_daenergy_dparameters(element)%slope = & o(nn + 2)%onedarray(1) do layer = 1, nn + 1 do p = 1, size(ohat(layer)%onedarray) do q = 1, size(delta(layer)%onedarray) unraveled_daenergy_dparameters(element)%weights(& layer)%twodarray(p, q) = & unraveled_parameters(element)%slope * & ohat(layer)%onedarray(p) * delta(layer)%onedarray(q) end do end do end do deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(delta) deallocate(delta(p)%onedarray) end do deallocate(delta) do p = 1, size(D) deallocate(D(p)%onedarray) end do deallocate(D) ! changing the derivatives of the energy from derived-type ! form into vector k = 0 l = 0 do element = 1, num_elements if (element > 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) do p = 1, num_rows do q = 1, num_cols calculate_datomicenergy_dparameters(& l + (p - 1) * num_cols + q) = & unraveled_daenergy_dparameters(& element)%weights(j)%twodarray(p, q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements calculate_datomicenergy_dparameters(l + 2 * element - 1) = & unraveled_daenergy_dparameters(element)%intercept calculate_datomicenergy_dparameters(l + 2 * element) = & unraveled_daenergy_dparameters(element)%slope end do ! deallocating derived-type parameters do element = 1, num_elements deallocate(unraveled_parameters(element)%weights) deallocate(unraveled_daenergy_dparameters(element)%weights) end do end function calculate_datomicenergy_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns derivative of force w.r.t. parameters in the ! image-centered mode. function calculate_dforce_dparameters_(num_inputs, inputs, & inputs_, num_parameters, parameters) implicit none integer:: num_inputs, num_parameters double precision:: calculate_dforce_dparameters_(num_parameters) double precision:: parameters(num_parameters) double precision:: inputs(num_inputs) double precision:: inputs_(num_inputs) integer:: m, n, j, l, layer, p, q, nn, num_cols integer:: num_rows double precision:: temp1, temp2 integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: delta(:), D(:) type(real_one_d_array), allocatable:: doutputs_dinputs(:) double precision, allocatable:: dohat_dinputs(:) type(real_one_d_array), allocatable:: dD_dinputs(:) type (real_one_d_array), allocatable:: ddelta_dinputs(:) double precision, allocatable:: & doutput_dinputsdweights(:, :) double precision, allocatable:: temp(:), temp3(:), temp4(:) double precision, allocatable:: temp5(:), temp6(:) type(real_two_d_array), allocatable:: weights(:) double precision:: intercept double precision:: slope type(real_two_d_array), allocatable:: unraveled_dforce_dweights(:) double precision:: dforce_dintercept double precision:: dforce_dslope ! changing the form of parameters from vector into derived-types l = 0 allocate(weights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols weights(j)%twodarray(p, q) = & parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do intercept = parameters(l + 1) slope = parameters(l + 2) dforce_dintercept = 0.d0 dforce_dslope = 0.d0 l = 0 allocate(unraveled_dforce_dweights(no_layers_of_elements(1)-1)) do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) allocate(unraveled_dforce_dweights(j)%twodarray(num_rows, & num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_dforce_dweights(j)%twodarray(p, q) = 0.0d0 end do end do l = l + num_rows * num_cols end do allocate(hiddensizes(no_layers_of_elements(1) - 2)) do m = 1, no_layers_of_elements(1) - 2 hiddensizes(m) = no_nodes_of_elements(m + 1) end do allocate(o(no_layers_of_elements(1))) allocate(ohat(no_layers_of_elements(1))) layer = 1 allocate(o(1)%onedarray(num_inputs)) allocate(ohat(1)%onedarray(num_inputs + 1)) do m = 1, num_inputs o(1)%onedarray(m) = inputs(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(weights(layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(& size(weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(weights(layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(weights(layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * & weights(layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(& size(weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do nn = size(o) - 2 allocate(D(nn + 1)) do layer = 1, nn + 1 allocate(D(layer)%onedarray(size(o(layer + 1)%onedarray))) do j = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then D(layer)%onedarray(j) = (1.0d0 - & o(layer + 1)%onedarray(j)* o(layer + 1)%onedarray(j)) elseif (activation_signal == 2) then D(layer)%onedarray(j) = o(layer + 1)%onedarray(j) * & (1.0d0 - o(layer + 1)%onedarray(j)) elseif (activation_signal == 3) then D(layer)%onedarray(j) = 1.0d0 end if end do end do allocate(delta(nn + 1)) allocate(delta(nn + 1)%onedarray(1)) delta(nn + 1)%onedarray(1) = D(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(delta(layer)%onedarray(size(D(layer)%onedarray))) do p = 1, size(D(layer)%onedarray) delta(layer)%onedarray(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp1 = D(layer)%onedarray(p) * weights(& layer + 1)%twodarray(p, q) temp2 = temp1 * delta(layer + 1)%onedarray(q) delta(layer)%onedarray(p) = & delta(layer)%onedarray(p) + temp2 end do end do end do allocate(doutputs_dinputs(nn + 2)) allocate(doutputs_dinputs(1)%onedarray(num_inputs)) do m = 1, num_inputs doutputs_dinputs(1)%onedarray(m) = inputs_(m) end do do layer = 1, nn + 1 allocate(temp(size(weights(layer)%twodarray, dim = 2))) do p = 1, size(weights(layer)%twodarray, dim = 2) temp(p) = 0.0d0 do q = 1, size(weights(layer)%twodarray, dim = 1) - 1 temp(p) = temp(p) + doutputs_dinputs(& layer)%onedarray(q) * weights(layer)%twodarray(q, p) end do end do q = size(o(layer + 1)%onedarray) allocate(doutputs_dinputs(layer + 1)%onedarray(q)) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then doutputs_dinputs(layer + 1)%onedarray(p) = temp(p) * & (1.0d0 - o(layer + 1)%onedarray(p) * & o(layer + 1)%onedarray(p)) else if (activation_signal == 2) then doutputs_dinputs(layer + 1)%onedarray(p) = & temp(p) * (1.0d0 - o(layer + 1)%onedarray(p)) * & o(layer + 1)%onedarray(p) else if (activation_signal == 3) then doutputs_dinputs(layer+ 1)%onedarray(p) = temp(p) end if end do deallocate(temp) end do allocate(dD_dinputs(nn + 1)) do layer = 1, nn + 1 allocate(dD_dinputs(layer)%onedarray(& size(o(layer + 1)%onedarray))) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then dD_dinputs(layer)%onedarray(p) = & - 2.0d0 * o(layer + 1)%onedarray(p) * & doutputs_dinputs(layer + 1)%onedarray(p) elseif (activation_signal == 2) then dD_dinputs(layer)%onedarray(p) = & doutputs_dinputs(layer + 1)%onedarray(p) * & (1.0d0 - 2.0d0 * o(layer + 1)%onedarray(p)) elseif (activation_signal == 3) then dD_dinputs(layer)%onedarray(p) =0.0d0 end if end do end do allocate(ddelta_dinputs(nn + 1)) allocate(ddelta_dinputs(nn + 1)%onedarray(1)) ddelta_dinputs(nn + 1)%onedarray(1) = & dD_dinputs(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(temp3(& size(weights(layer + 1)%twodarray, dim = 1) - 1)) allocate(temp4(& size(weights(layer + 1)%twodarray, dim = 1) - 1)) do p = 1, size(weights(layer + 1)%twodarray, dim = 1) - 1 temp3(p) = 0.0d0 temp4(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp3(p) = temp3(p) + weights(layer + 1)%twodarray(& p, q) * delta(layer + 1)%onedarray(q) temp4(p) = temp4(p) + weights(layer + 1)%twodarray(& p, q) * ddelta_dinputs(layer + 1)%onedarray(q) end do end do allocate(temp5(size(dD_dinputs(layer)%onedarray))) allocate(temp6(size(dD_dinputs(layer)%onedarray))) allocate(ddelta_dinputs(layer)%onedarray(& size(dD_dinputs(layer)%onedarray))) do p = 1, size(dD_dinputs(layer)%onedarray) temp5(p) = & dD_dinputs(layer)%onedarray(p) * temp3(p) temp6(p) = D(layer)%onedarray(p) * temp4(p) ddelta_dinputs(layer)%onedarray(p)= & temp5(p) + temp6(p) end do deallocate(temp3) deallocate(temp4) deallocate(temp5) deallocate(temp6) end do dforce_dslope = doutputs_dinputs(nn + 2)%onedarray(1) ! force is multiplied by -1, because it is -dE/dx and not dE/dx. dforce_dslope = -1.0d0 * dforce_dslope do layer = 1, nn + 1 allocate(dohat_dinputs(& size(doutputs_dinputs(layer)%onedarray) + 1)) do p = 1, size(doutputs_dinputs(layer)%onedarray) dohat_dinputs(p) = & doutputs_dinputs(layer)%onedarray(p) end do dohat_dinputs(& size(doutputs_dinputs(layer)%onedarray) + 1) = 0.0d0 allocate(doutput_dinputsdweights(& size(dohat_dinputs), size(delta(layer)%onedarray))) do p = 1, size(dohat_dinputs) do q = 1, size(delta(layer)%onedarray) doutput_dinputsdweights(p, q)= 0.0d0 end do end do do p = 1, size(dohat_dinputs) do q = 1, size(delta(layer)%onedarray) doutput_dinputsdweights(p, q) = & doutput_dinputsdweights(p, q) + & dohat_dinputs(p) * delta(layer)%onedarray(q) + & ohat(layer)%onedarray(p)* & ddelta_dinputs(layer)%onedarray(q) end do end do do p = 1, size(ohat(layer)%onedarray) do q = 1, size(delta(layer)%onedarray) unraveled_dforce_dweights(layer)%twodarray(p, q) = & slope * doutput_dinputsdweights(p, q) ! force is multiplied by -1, because it is -dE/dx and ! not dE/dx. unraveled_dforce_dweights(layer)%twodarray(p, q) = & -1.0d0 * unraveled_dforce_dweights(layer)%twodarray(p, q) end do end do deallocate(dohat_dinputs) deallocate(doutput_dinputsdweights) end do ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(delta) deallocate(delta(p)%onedarray) end do deallocate(delta) do p = 1, size(D) deallocate(D(p)%onedarray) end do deallocate(D) do p = 1, size(doutputs_dinputs) deallocate(doutputs_dinputs(p)%onedarray) end do deallocate(doutputs_dinputs) do p = 1, size(ddelta_dinputs) deallocate(ddelta_dinputs(p)%onedarray) end do deallocate(ddelta_dinputs) do p = 1, size(dD_dinputs) deallocate(dD_dinputs(p)%onedarray) end do deallocate(dD_dinputs) l = 0 do j = 1, no_layers_of_elements(1) - 1 num_rows = no_nodes_of_elements(j) + 1 num_cols = no_nodes_of_elements(j + 1) do p = 1, num_rows do q = 1, num_cols calculate_dforce_dparameters_(& l + (p - 1) * num_cols + q) = & unraveled_dforce_dweights(j)%twodarray(p, q) end do end do l = l + num_rows * num_cols end do calculate_dforce_dparameters_(l + 1) = dforce_dintercept calculate_dforce_dparameters_(l + 2) = dforce_dslope ! deallocating derived-type parameters do p = 1, size(weights) deallocate(weights(p)%twodarray) end do deallocate(weights) do p = 1, size(unraveled_dforce_dweights) deallocate(unraveled_dforce_dweights(p)%twodarray) end do deallocate(unraveled_dforce_dweights) end function calculate_dforce_dparameters_ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! Returns derivative of force w.r.t. parameters in the ! atom-centered mode function calculate_dforce_dparameters(symbol, len_of_fingerprint, & fingerprint, fingerprintprime, num_elements, elements_numbers, & num_parameters, parameters) implicit none integer:: symbol, len_of_fingerprint integer:: num_parameters, num_elements double precision:: fingerprint(len_of_fingerprint) double precision:: fingerprintprime(len_of_fingerprint) integer:: elements_numbers(num_elements) double precision:: parameters(num_parameters) double precision:: calculate_dforce_dparameters(num_parameters) integer:: element, m, n, j, k, l, layer, p, q, nn, num_cols integer:: num_rows double precision:: temp1, temp2 integer, allocatable:: hiddensizes(:) double precision, allocatable:: net(:) type(real_one_d_array), allocatable:: o(:), ohat(:) type(real_one_d_array), allocatable:: delta(:), D(:) type(real_one_d_array), allocatable:: doutputs_dinputs(:) double precision, allocatable:: dohat_dinputs(:) type(real_one_d_array), allocatable:: dD_dinputs(:) type (real_one_d_array), allocatable:: ddelta_dinputs(:) double precision, allocatable:: & doutput_dinputsdweights(:, :) double precision, allocatable:: temp(:), temp3(:), temp4(:) double precision, allocatable:: temp5(:), temp6(:) type(element_parameters):: unraveled_parameters(num_elements) type(element_parameters):: & unraveled_dforce_dparameters(num_elements) double precision:: fingerprint_(len_of_fingerprint) double precision:: fingerprintprime_(len_of_fingerprint) ! scaling fingerprints do element = 1, num_elements if (symbol == & elements_numbers(element)) then exit end if end do do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprint_(l) = -1.0d0 + 2.0d0 * & (fingerprint(l) - min_fingerprints(element, l)) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprint_(l) = fingerprint(l) endif end do ! scaling fingerprintprimes do l = 1, len_of_fingerprint if ((max_fingerprints(element, l) - & min_fingerprints(element, l)) .GT. & (10.0d0 ** (-8.0d0))) then fingerprintprime_(l) = & 2.0d0 * fingerprintprime(l) / & (max_fingerprints(element, l) - & min_fingerprints(element, l)) else fingerprintprime_(l) = fingerprintprime(l) endif end do ! changing the form of parameters from vector into derived-types k = 0 l = 0 do element = 1, num_elements allocate(unraveled_parameters(element)%weights(& no_layers_of_elements(element)-1)) if (element .GT. 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_parameters(& element)%weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_parameters(element)%weights(j)%twodarray(& p, q) = parameters(l + (p - 1) * num_cols + q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements unraveled_parameters(element)%intercept = & parameters(l + 2 * element - 1) unraveled_parameters(element)%slope = & parameters(l + 2 * element) end do do element = 1, num_elements unraveled_dforce_dparameters(element)%intercept = 0.d0 unraveled_dforce_dparameters(element)%slope = 0.d0 end do k = 0 l = 0 do element = 1, num_elements allocate(unraveled_dforce_dparameters(element)%weights(& no_layers_of_elements(element)-1)) if (element > 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) allocate(unraveled_dforce_dparameters(& element)%weights(j)%twodarray(num_rows, num_cols)) do p = 1, num_rows do q = 1, num_cols unraveled_dforce_dparameters(& element)%weights(j)%twodarray(p, q) = 0.0d0 end do end do l = l + num_rows * num_cols end do end do p = 0 do element = 1, num_elements if (symbol == elements_numbers(element)) then exit else p = p + no_layers_of_elements(element) end if end do allocate(hiddensizes(no_layers_of_elements(element) - 2)) do m = 1, no_layers_of_elements(element) - 2 hiddensizes(m) = no_nodes_of_elements(p + m + 1) end do allocate(o(no_layers_of_elements(element))) allocate(ohat(no_layers_of_elements(element))) layer = 1 allocate(o(1)%onedarray(len_of_fingerprint)) allocate(ohat(1)%onedarray(len_of_fingerprint + 1)) do m = 1, len_of_fingerprint o(1)%onedarray(m) = fingerprint_(m) end do do layer = 1, size(hiddensizes) + 1 do m = 1, size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=1) - 1 ohat(layer)%onedarray(m) = o(layer)%onedarray(m) end do ohat(layer)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=1)) = 1.0d0 allocate(net(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(o(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2))) allocate(ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1)) do m = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=2) net(m) = 0.0d0 do n = 1, size(unraveled_parameters(element)%weights(& layer)%twodarray, dim=1) net(m) = net(m) + & ohat(layer)%onedarray(n) * & unraveled_parameters(element)%weights(& layer)%twodarray(n, m) end do if (activation_signal == 1) then o(layer + 1)%onedarray(m) = tanh(net(m)) else if (activation_signal == 2) then o(layer + 1)%onedarray(m) = & 1.0d0 / (1.0d0 + exp(- net(m))) else if (activation_signal == 3) then o(layer + 1)%onedarray(m) = net(m) end if ohat(layer + 1)%onedarray(m) = o(layer + 1)%onedarray(m) end do ohat(layer + 1)%onedarray(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim=2) + 1) = 1.0d0 deallocate(net) end do nn = size(o) - 2 allocate(D(nn + 1)) do layer = 1, nn + 1 allocate(D(layer)%onedarray(size(o(layer + 1)%onedarray))) do j = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then D(layer)%onedarray(j) = & (1.0d0 - o(layer + 1)%onedarray(j)* & o(layer + 1)%onedarray(j)) elseif (activation_signal == 2) then D(layer)%onedarray(j) = o(layer + 1)%onedarray(j) * & (1.0d0 - o(layer + 1)%onedarray(j)) elseif (activation_signal == 3) then D(layer)%onedarray(j) = 1.0d0 end if end do end do allocate(delta(nn + 1)) allocate(delta(nn + 1)%onedarray(1)) delta(nn + 1)%onedarray(1) = D(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(delta(layer)%onedarray(size(D(layer)%onedarray))) do p = 1, size(D(layer)%onedarray) delta(layer)%onedarray(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp1 = D(layer)%onedarray(p) * & unraveled_parameters(element)%weights(& layer + 1)%twodarray(p, q) temp2 = temp1 * delta(layer + 1)%onedarray(q) delta(layer)%onedarray(p) = & delta(layer)%onedarray(p) + temp2 end do end do end do allocate(doutputs_dinputs(nn + 2)) allocate(doutputs_dinputs(1)%onedarray(& len_of_fingerprint)) do m = 1, len_of_fingerprint doutputs_dinputs(1)%onedarray(m) = fingerprintprime_(m) end do do layer = 1, nn + 1 allocate(temp(size(unraveled_parameters(& element)%weights(layer)%twodarray, dim = 2))) do p = 1, size(unraveled_parameters(& element)%weights(layer)%twodarray, dim = 2) temp(p) = 0.0d0 do q = 1, size(unraveled_parameters(& element)%weights(layer)%twodarray, dim = 1) - 1 temp(p) = temp(p) + doutputs_dinputs(& layer)%onedarray(q) * unraveled_parameters(& element)%weights(layer)%twodarray(q, p) end do end do q = size(o(layer + 1)%onedarray) allocate(doutputs_dinputs(layer + 1)%onedarray(q)) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then doutputs_dinputs(layer + 1)%onedarray(p) = temp(p) * & (1.0d0 - o(layer + 1)%onedarray(p) * & o(layer + 1)%onedarray(p)) else if (activation_signal == 2) then doutputs_dinputs(layer + 1)%onedarray(p) = temp(p) * & (1.0d0 - o(layer + 1)%onedarray(p)) * & o(layer + 1)%onedarray(p) else if (activation_signal == 3) then doutputs_dinputs(layer+ 1)%onedarray(p) = temp(p) end if end do deallocate(temp) end do allocate(dD_dinputs(nn + 1)) do layer = 1, nn + 1 allocate(dD_dinputs(layer)%onedarray(& size(o(layer + 1)%onedarray))) do p = 1, size(o(layer + 1)%onedarray) if (activation_signal == 1) then dD_dinputs(layer)%onedarray(p) =- 2.0d0 * & o(layer + 1)%onedarray(p) * & doutputs_dinputs(layer + 1)%onedarray(p) elseif (activation_signal == 2) then dD_dinputs(layer)%onedarray(p) = & doutputs_dinputs(layer + 1)%onedarray(p) * & (1.0d0 - 2.0d0 * o(layer + 1)%onedarray(p)) elseif (activation_signal == 3) then dD_dinputs(layer)%onedarray(p) =0.0d0 end if end do end do allocate(ddelta_dinputs(nn + 1)) allocate(ddelta_dinputs(nn + 1)%onedarray(1)) ddelta_dinputs(nn + 1)%onedarray(1) = & dD_dinputs(nn + 1)%onedarray(1) do layer = nn, 1, -1 allocate(temp3(size(unraveled_parameters(element)%weights(& layer + 1)%twodarray, dim = 1) - 1)) allocate(temp4(size(unraveled_parameters(element)%weights(& layer + 1)%twodarray, dim = 1) - 1)) do p = 1, size(unraveled_parameters(element)%weights(& layer + 1)%twodarray, dim = 1) - 1 temp3(p) = 0.0d0 temp4(p) = 0.0d0 do q = 1, size(delta(layer + 1)%onedarray) temp3(p) = temp3(p) + unraveled_parameters(& element)%weights(layer + 1)%twodarray(p, q) * & delta(layer + 1)%onedarray(q) temp4(p) = temp4(p) + unraveled_parameters(& element)%weights(layer + 1)%twodarray(p, q) * & ddelta_dinputs(layer + 1)%onedarray(q) end do end do allocate(temp5(size(dD_dinputs(layer)%onedarray))) allocate(temp6(size(dD_dinputs(layer)%onedarray))) allocate(ddelta_dinputs(layer)%onedarray(& size(dD_dinputs(layer)%onedarray))) do p = 1, size(dD_dinputs(layer)%onedarray) temp5(p) = & dD_dinputs(layer)%onedarray(p) * temp3(p) temp6(p) = D(layer)%onedarray(p) * temp4(p) ddelta_dinputs(layer)%onedarray(p)= & temp5(p) + temp6(p) end do deallocate(temp3) deallocate(temp4) deallocate(temp5) deallocate(temp6) end do unraveled_dforce_dparameters(element)%slope = & doutputs_dinputs(nn + 2)%onedarray(1) ! force is multiplied by -1, because it is -dE/dx and not dE/dx. unraveled_dforce_dparameters(element)%slope = & -1.0d0 * unraveled_dforce_dparameters(element)%slope do layer = 1, nn + 1 allocate(dohat_dinputs(& size(doutputs_dinputs(layer)%onedarray) + 1)) do p = 1, size(doutputs_dinputs(layer)%onedarray) dohat_dinputs(p) = & doutputs_dinputs(layer)%onedarray(p) end do dohat_dinputs(& size(doutputs_dinputs(layer)%onedarray) + 1) = 0.0d0 allocate(doutput_dinputsdweights(& size(dohat_dinputs), size(delta(layer)%onedarray))) do p = 1, size(dohat_dinputs) do q = 1, size(delta(layer)%onedarray) doutput_dinputsdweights(p, q)= 0.0d0 end do end do do p = 1, size(dohat_dinputs) do q = 1, size(delta(layer)%onedarray) doutput_dinputsdweights(p, q) = & doutput_dinputsdweights(p, q) + & dohat_dinputs(p) * delta(layer)%onedarray(q) + & ohat(layer)%onedarray(p)* & ddelta_dinputs(layer)%onedarray(q) end do end do do p = 1, size(ohat(layer)%onedarray) do q = 1, size(delta(layer)%onedarray) unraveled_dforce_dparameters(element)%weights(& layer)%twodarray(p, q) = & unraveled_parameters(element)%slope * & doutput_dinputsdweights(p, q) ! force is multiplied by -1, because it is -dE/dx and ! not dE/dx. unraveled_dforce_dparameters(element)%weights(& layer)%twodarray(p, q) = & -1.0d0 * unraveled_dforce_dparameters(element)%weights(& layer)%twodarray(p, q) end do end do deallocate(dohat_dinputs) deallocate(doutput_dinputsdweights) end do ! deallocating neural network deallocate(hiddensizes) do p = 1, size(o) deallocate(o(p)%onedarray) end do deallocate(o) do p = 1, size(ohat) deallocate(ohat(p)%onedarray) end do deallocate(ohat) do p = 1, size(delta) deallocate(delta(p)%onedarray) end do deallocate(delta) do p = 1, size(D) deallocate(D(p)%onedarray) end do deallocate(D) do p = 1, size(doutputs_dinputs) deallocate(doutputs_dinputs(p)%onedarray) end do deallocate(doutputs_dinputs) do p = 1, size(ddelta_dinputs) deallocate(ddelta_dinputs(p)%onedarray) end do deallocate(ddelta_dinputs) do p = 1, size(dD_dinputs) deallocate(dD_dinputs(p)%onedarray) end do deallocate(dD_dinputs) k = 0 l = 0 do element = 1, num_elements if (element > 1) then k = k + no_layers_of_elements(element - 1) end if do j = 1, no_layers_of_elements(element) - 1 num_rows = no_nodes_of_elements(k + j) + 1 num_cols = no_nodes_of_elements(k + j + 1) do p = 1, num_rows do q = 1, num_cols calculate_dforce_dparameters(& l + (p - 1) * num_cols + q) = & unraveled_dforce_dparameters(& element)%weights(j)%twodarray(p, q) end do end do l = l + num_rows * num_cols end do end do do element = 1, num_elements calculate_dforce_dparameters(l + 2 * element - 1) = & unraveled_dforce_dparameters(element)%intercept calculate_dforce_dparameters(l + 2 * element) = & unraveled_dforce_dparameters(element)%slope end do ! deallocating derived-type parameters do element = 1, num_elements deallocate(unraveled_parameters(element)%weights) deallocate(unraveled_dforce_dparameters(element)%weights) end do end function calculate_dforce_dparameters !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! end module neuralnetwork !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! andrewpeterson-amp-4878fc892f2c/amp/model/neuralnetwork.py000066400000000000000000001347061332417112400236100ustar00rootroot00000000000000import os import numpy as np from collections import OrderedDict from ase.calculators.calculator import Parameters from . import LossFunction, calculate_fingerprints_range, Model from ..regression import Regressor from ..utilities import Logger, hash_images, make_filename class NeuralNetwork(Model): """Class that implements a basic feed-forward neural network. Parameters ---------- hiddenlayers : dict Dictionary of chemical element symbols and architectures of their corresponding hidden layers of the conventional neural network. Number of nodes of last layer is always one corresponding to energy. However, number of nodes of first layer is equal to three times number of atoms in the system in the case of no descriptor, and is equal to length of symmetry functions of the descriptor. Can be fed using tuples as: >>> hiddenlayers = (3, 2,) for example, in which a neural network with two hidden layers, the first one having three nodes and the second one having two nodes is assigned (to the whole atomic system in the no descriptor case, and to each chemical element in the atom-centered mode). When setting only one hidden layer, the dictionary can be fed as: >>> hiddenlayers = (3,) In the atom-centered mode, neural network for each species can be assigned seperately, as: >>> hiddenlayers = {"O":(3,5), "Au":(5,6)} for example. activation : str Assigns the type of activation funtion. "linear" refers to linear function, "tanh" refers to tanh function, and "sigmoid" refers to sigmoid function. weights : dict In the case of no descriptor, keys correspond to layers and values are two dimensional arrays of network weight. In the atom-centered mode, keys correspond to chemical elements and values are dictionaries with layer keys and network weight two dimensional arrays as values. Arrays are set up to connect node i in the previous layer with node j in the current layer with indices w[i,j]. The last value for index i corresponds to bias. If weights is not given, arrays will be randomly generated. scalings : dict In the case of no descriptor, keys are "intercept" and "slope" and values are real numbers. In the fingerprinting scheme, keys correspond to chemical elements and values are dictionaries with "intercept" and "slope" keys and real number values. If scalings is not given, it will be randomly generated. fprange : dict Range of fingerprints of each chemical species. Should be fed as a dictionary of chemical species and a list of minimum and maximun, e.g.: >>> fprange={"Pd": [0.31, 0.59], "O":[0.56, 0.72]} regressor : object Regressor object for finding best fit model parameters, e.g. by loss function optimization via amp.regression.Regressor. mode : str Can be either 'atom-centered' or 'image-centered'. lossfunction : object Loss function object, if at all desired by the user. version : object Version of this class. fortran : bool If True, allows for extrapolation, if False, does not allow. checkpoints : int Frequency with which to save parameter checkpoints upon training. E.g., 100 saves a checkpoint on each 100th training setp. Specify None for no checkpoints. Note: You can make this negative to not overwrite previous checkpoints. .. note:: Dimensions of weight two dimensional arrays should be consistent with hiddenlayers. Raises ------ RuntimeError, NotImplementedError """ def __init__(self, hiddenlayers=(5, 5), activation='tanh', weights=None, scalings=None, fprange=None, regressor=None, mode=None, lossfunction=None, version=None, fortran=True, checkpoints=100): # Version check, particularly if restarting. compatibleversions = ['2015.12', ] if (version is not None) and version not in compatibleversions: raise RuntimeError('Error: Trying to use NeuralNetwork' ' version %s, but this module only supports' ' versions %s. You may need an older or ' 'newer version of Amp.' % (version, compatibleversions)) else: version = compatibleversions[-1] # The parameters dictionary contains the minimum information # to produce a compatible model; e.g., one that gives # the identical energy (and/or forces) when fed a fingerprint. p = self.parameters = Parameters() p.importname = '.model.neuralnetwork.NeuralNetwork' p.version = version p.hiddenlayers = hiddenlayers p.weights = weights p.scalings = scalings p.fprange = fprange p.activation = activation p.mode = mode # Checking that the activation function is given correctly: if activation not in ['linear', 'tanh', 'sigmoid']: _ = ('Unknown activation function %s; must be one of ' '"linear", "tanh", or "sigmoid".' % activation) raise NotImplementedError(_) self.regressor = regressor self.parent = None # Can hold a reference to main Amp instance. self.lossfunction = lossfunction self.fortran = fortran self.checkpoints = checkpoints if self.lossfunction is None: self.lossfunction = LossFunction() def fit(self, trainingimages, descriptor, log, parallel, only_setup=False, ): """Fit the model parameters such that the fingerprints can be used to describe the energies in trainingimages. log is the logging object. descriptor is a descriptor object, as would be in calc.descriptor. Parameters ---------- trainingimages : dict Hashed dictionary of training images. descriptor : object Class representing local atomic environment. log : Logger object Write function at which to log data. Note this must be a callable function. parallel: dict Parallel configuration dictionary. Takes the same form as in amp.Amp. only_setup : bool only_setup is primarily for debugging. It initializes all variables but skips the last line of starting the regressor. """ # Set all parameters and report to logfile. self._parallel = parallel self._log = log if self.regressor is None: self.regressor = Regressor() p = self.parameters tp = self.trainingparameters = Parameters() tp.trainingimages = trainingimages tp.descriptor = descriptor if p.mode is None: p.mode = descriptor.parameters.mode else: assert p.mode == descriptor.parameters.mode log('Regression in %s mode.' % p.mode) if 'fprange' not in p or p.fprange is None: log('Calculating new fingerprint range; this range is part ' 'of the model.') p.fprange = calculate_fingerprints_range(descriptor, trainingimages) if p.mode == 'atom-centered': # If hiddenlayers is a tuple/list, convert to a dictionary. if not hasattr(p.hiddenlayers, 'keys'): p.hiddenlayers = {element: p.hiddenlayers for element in p.fprange.keys()} log('Hidden-layer structure:') if p.mode == 'image-centered': log(' %s' % str(p.hiddenlayers)) elif p.mode == 'atom-centered': for item in p.hiddenlayers.items(): log(' %2s: %s' % item) if p.weights is None: log('Initializing with random weights.') if p.mode == 'image-centered': raise NotImplementedError('Needs to be coded.') elif p.mode == 'atom-centered': p.weights = get_random_weights(p.hiddenlayers, p.activation, None, p.fprange) else: log('Initial weights already present.') if p.scalings is None: log('Initializing with random scalings.') if p.mode == 'image-centered': raise NotImplementedError('Need to code.') elif p.mode == 'atom-centered': p.scalings = get_random_scalings(trainingimages, p.activation, p.fprange.keys()) else: log('Initial scalings already present.') if only_setup: return # Regress the model. self.step = 0 result = self.regressor.regress(model=self, log=log) return result # True / False @property def forcetraining(self): """Returns true if forcetraining is turned on (as determined by examining the convergence criteria in the loss function), else returns False. """ if self.lossfunction.parameters['force_coefficient'] is None: forcetraining = False elif self.lossfunction.parameters['force_coefficient'] > 0.: forcetraining = True return forcetraining @property def vector(self): """Access to get or set the model parameters (weights, scaling for each network) as a single vector, useful in particular for regression. Parameters ---------- vector : list Parameters of the regression model in the form of a list. """ if self.parameters['weights'] is None: return None p = self.parameters if not hasattr(self, 'ravel'): self.ravel = Raveler(p.weights, p.scalings) return self.ravel.to_vector(weights=p.weights, scalings=p.scalings) @vector.setter def vector(self, vector): p = self.parameters if not hasattr(self, 'ravel'): self.ravel = Raveler(p.weights, p.scalings) weights, scalings = self.ravel.to_dicts(vector) p['weights'] = weights p['scalings'] = scalings def get_loss(self, vector): """Method to be called by the regression master. Takes one and only one input, a vector of parameters. Returns one output, the value of the loss (cost) function. Parameters ---------- vector : list Parameters of the regression model in the form of a list. """ if self.step == 0: filename = make_filename(self.parent.label, '-initial-parameters.amp') filename = self.parent.save(filename, overwrite=True) if self.checkpoints: if self.step % self.checkpoints == 0: self._log('Saving checkpoint data.') if self.checkpoints < 0: path = os.path.join(self.parent.label + '-checkpoints') if self.step == 0: if not os.path.exists(path): os.mkdir(path) filename = os.path.join(path, '{}.amp'.format(int(self.step))) else: filename = make_filename(self.parent.label, '-checkpoint.amp') self.parent.save(filename, overwrite=True) loss = self.lossfunction.get_loss(vector, lossprime=False)['loss'] if hasattr(self, 'observer'): self.observer(self, vector, loss) self.step += 1 return loss def get_lossprime(self, vector): """Method to be called by the regression master. Takes one and only one input, a vector of parameters. Returns one output, the value of the derivative of the loss function with respect to model parameters. Parameters ---------- vector : list Parameters of the regression model in the form of a list. """ return self.lossfunction.get_loss(vector, lossprime=True)['dloss_dparameters'] @property def lossfunction(self): """Allows the user to set a custom loss function. For example, >>> from amp.model import LossFunction >>> lossfxn = LossFunction(energy_tol=0.0001) >>> calc.model.lossfunction = lossfxn Parameters ---------- lossfunction : object Loss function object, if at all desired by the user. """ return self._lossfunction @lossfunction.setter def lossfunction(self, lossfunction): if hasattr(lossfunction, 'attach_model'): lossfunction.attach_model(self) # Allows access to methods. self._lossfunction = lossfunction def calculate_atomic_energy(self, afp, index, symbol,): """ Given input to the neural network, output (which corresponds to energy) is calculated about the specified atom. The sum of these for all atoms is the total energy (in atom-centered mode). Parameters --------- afp : list Atomic fingerprints in the form of a list to be used as input to the neural network. index: int Index of the atom for which atomic energy is calculated (only used in the atom-centered mode). symbol : str Symbol of the atom for which atomic energy is calculated (only used in the atom-centered mode). Returns ------- float Energy. """ if self.parameters.mode != 'atom-centered': raise AssertionError('calculate_atomic_energy should only be ' ' called in atom-centered mode.') scaling = self.parameters.scalings[symbol] outputs = calculate_nodal_outputs(self.parameters, afp, symbol,) atomic_amp_energy = scaling['slope'] * \ float(outputs[len(outputs) - 1]) + \ scaling['intercept'] return atomic_amp_energy def calculate_force(self, afp, derafp, direction, nindex=None, nsymbol=None,): """Given derivative of input to the neural network, derivative of output (which corresponds to forces) is calculated. Parameters ---------- afp : list Atomic fingerprints in the form of a list to be used as input to the neural network. derafp : list Derivatives of atomic fingerprints in the form of a list to be used as input to the neural network. direction : int Direction of force. nindex : int Index of the neighbor atom which force is acting at. (only used in the atom-centered mode) nsymbol : str Symbol of the neighbor atom which force is acting at. (only used in the atom-centered mode) Returns ------- float Force. """ scaling = self.parameters.scalings[nsymbol] outputs = calculate_nodal_outputs(self.parameters, afp, nsymbol,) dOutputs_dInputs = calculate_dOutputs_dInputs(self.parameters, derafp, outputs, nsymbol,) force = float((scaling['slope'] * dOutputs_dInputs[len(dOutputs_dInputs) - 1][0])) # force is multiplied by -1, because it is -dE/dx and not dE/dx. force *= -1. return force def calculate_dAtomicEnergy_dParameters(self, afp, index=None, symbol=None): """Returns the derivative of energy square error with respect to variables. Parameters ---------- afp : list Atomic fingerprints in the form of a list to be used as input to the neural network. index : int Index of the atom for which atomic energy is calculated (only used in the atom-centered mode) symbol : str Symbol of the atom for which atomic energy is calculated (only used in the atom-centered mode) Returns ------- list of float The value of the derivative of energy square error with respect to variables. """ p = self.parameters scaling = p.scalings[symbol] # self.W dictionary initiated. self.W = {} for elm in p.weights.keys(): self.W[elm] = {} weight = p.weights[elm] for _ in range(len(weight)): self.W[elm][_ + 1] = np.delete(weight[_ + 1], -1, 0) W = self.W[symbol] dAtomicEnergy_dParameters = np.zeros(self.ravel.count) dAtomicEnergy_dWeights, dAtomicEnergy_dScalings = \ self.ravel.to_dicts(dAtomicEnergy_dParameters) outputs = calculate_nodal_outputs(self.parameters, afp, symbol,) ohat, D, delta = calculate_ohat_D_delta(self.parameters, outputs, W) dAtomicEnergy_dScalings[symbol]['intercept'] = 1. dAtomicEnergy_dScalings[symbol][ 'slope'] = float(outputs[len(outputs) - 1]) for k in range(1, len(outputs)): dAtomicEnergy_dWeights[symbol][k] = float(scaling['slope']) * \ np.dot(np.matrix(ohat[k - 1]).T, np.matrix(delta[k]).T) dAtomicEnergy_dParameters = \ self.ravel.to_vector( dAtomicEnergy_dWeights, dAtomicEnergy_dScalings) return dAtomicEnergy_dParameters def calculate_dForce_dParameters(self, afp, derafp, direction, nindex=None, nsymbol=None,): """Returns the derivative of force square error with respect to variables. Parameters ---------- afp : list Atomic fingerprints in the form of a list to be used as input to the neural network. derafp : list Derivatives of atomic fingerprints in the form of a list to be used as input to the neural network. direction : int Direction of force. nindex : int Index of the neighbor atom which force is acting at. (only used in the atom-centered mode) nsymbol : str Symbol of the neighbor atom which force is acting at. (only used in the atom-centered mode) Returns ------- list of float The value of the derivative of force square error with respect to variables. """ p = self.parameters scaling = p.scalings[nsymbol] activation = p.activation # self.W dictionary initiated. self.W = {} for elm in p.weights.keys(): self.W[elm] = {} weight = p.weights[elm] for _ in range(len(weight)): self.W[elm][_ + 1] = np.delete(weight[_ + 1], -1, 0) W = self.W[nsymbol] dForce_dParameters = np.zeros(self.ravel.count) dForce_dWeights, dForce_dScalings = \ self.ravel.to_dicts(dForce_dParameters) outputs = calculate_nodal_outputs(self.parameters, afp, nsymbol,) ohat, D, delta = calculate_ohat_D_delta(self.parameters, outputs, W) dOutputs_dInputs = calculate_dOutputs_dInputs(self.parameters, derafp, outputs, nsymbol,) N = len(outputs) - 2 dD_dInputs = {} for k in range(1, N + 2): # Calculating coordinate derivative of D matrix dD_dInputs[k] = np.zeros(shape=(np.size(outputs[k]), np.size(outputs[k]))) for j in range(np.size(outputs[k])): if activation == 'linear': # linear dD_dInputs[k][j, j] = 0. elif activation == 'tanh': # tanh dD_dInputs[k][j, j] = \ - 2. * outputs[k][0, j] * dOutputs_dInputs[k][j] elif activation == 'sigmoid': # sigmoid dD_dInputs[k][j, j] = dOutputs_dInputs[k][j] - \ 2. * outputs[k][0, j] * dOutputs_dInputs[k][j] # Calculating coordinate derivative of delta dDelta_dInputs = {} # output layer dDelta_dInputs[N + 1] = dD_dInputs[N + 1] # hidden layers temp1 = {} temp2 = {} for k in range(N, 0, -1): temp1[k] = np.dot(W[k + 1], delta[k + 1]) temp2[k] = np.dot(W[k + 1], dDelta_dInputs[k + 1]) dDelta_dInputs[k] = \ np.dot(dD_dInputs[k], temp1[k]) + np.dot(D[k], temp2[k]) # Calculating coordinate derivative of ohat and # coordinates weights derivative of atomic_output dOhat_dInputs = {} dOutput_dInputsdWeights = {} for k in range(1, N + 2): dOhat_dInputs[k - 1] = [None] * (1 + len(dOutputs_dInputs[k - 1])) bound = len(dOutputs_dInputs[k - 1]) for count in range(bound): dOhat_dInputs[k - 1][count] = dOutputs_dInputs[k - 1][count] dOhat_dInputs[k - 1][count + 1] = 0. dOutput_dInputsdWeights[k] = \ np.dot(np.matrix(dOhat_dInputs[k - 1]).T, np.matrix(delta[k]).T) + \ np.dot(np.matrix(ohat[k - 1]).T, np.matrix(dDelta_dInputs[k]).T) for k in range(1, N + 2): dForce_dWeights[nsymbol][k] = float(scaling['slope']) * \ dOutput_dInputsdWeights[k] dForce_dScalings[nsymbol]['slope'] = dOutputs_dInputs[N + 1][0] dForce_dParameters = self.ravel.to_vector(dForce_dWeights, dForce_dScalings) # force is multiplied by -1, because it is -dE/dx and not dE/dx. dForce_dParameters *= -1. return dForce_dParameters # Auxiliary functions ######################################################### def calculate_nodal_outputs(parameters, afp, symbol,): """ Given input to the neural network, output (which corresponds to energy) is calculated about the specified atom. The sum of these for all atoms is the total energy (in atom-centered mode). Parameters ---------- parameters : dict ASE dictionary object. afp : list Atomic fingerprints in the form of a list to be used as input to the neural network. symbol : str Symbol of the atom for which atomic energy is calculated (only used in the atom-centered mode) Returns ------- dict Outputs of neural network nodes """ _afp = np.array(afp).copy() hiddenlayers = parameters.hiddenlayers[symbol] weight = parameters.weights[symbol] activation = parameters.activation fprange = parameters.fprange[symbol] # Scale the fingerprints to be in [-1, 1] range. for _ in range(np.shape(_afp)[0]): if (fprange[_][1] - fprange[_][0]) > (10.**(-8.)): _afp[_] = -1.0 + 2.0 * ((_afp[_] - fprange[_][0]) / (fprange[_][1] - fprange[_][0])) # Calculate node values. o = {} # node values layer = 1 # input layer net = {} # excitation ohat = {} # ohat is the nodal output matrix o concatenated by 1 for biases len_of_afp = len(_afp) # a temp variable is defined to construct the output matix o temp = np.zeros((1, len_of_afp + 1)) for _ in range(len_of_afp): temp[0, _] = _afp[_] temp[0, len(_afp)] = 1.0 ohat[0] = temp net[1] = np.dot(ohat[0], weight[1]) if activation == 'linear': o[1] = net[1] # linear activation elif activation == 'tanh': o[1] = np.tanh(net[1]) # tanh activation elif activation == 'sigmoid': # sigmoid activation o[1] = 1. / (1. + np.exp(-net[1])) temp = np.zeros((1, np.shape(o[1])[1] + 1)) bound = np.shape(o[1])[1] for _ in range(bound): temp[0, _] = o[1][0, _] temp[0, np.shape(o[1])[1]] = 1.0 ohat[1] = temp for hiddenlayer in hiddenlayers[1:]: layer += 1 net[layer] = np.dot(ohat[layer - 1], weight[layer]) if activation == 'linear': o[layer] = net[layer] # linear activation elif activation == 'tanh': o[layer] = np.tanh(net[layer]) # tanh activation elif activation == 'sigmoid': # sigmoid activation o[layer] = 1. / (1. + np.exp(-net[layer])) temp = np.zeros((1, np.size(o[layer]) + 1)) bound = np.size(o[layer]) for _ in range(bound): temp[0, _] = o[layer][0, _] temp[0, np.size(o[layer])] = 1.0 ohat[layer] = temp layer += 1 # output layer net[layer] = np.dot(ohat[layer - 1], weight[layer]) if activation == 'linear': o[layer] = net[layer] # linear activation elif activation == 'tanh': o[layer] = np.tanh(net[layer]) # tanh activation elif activation == 'sigmoid': # sigmoid activation o[layer] = 1. / (1. + np.exp(-net[layer])) del hiddenlayers, weight, ohat, net len_of_afp = len(_afp) temp = np.zeros((1, len_of_afp)) for _ in range(len_of_afp): temp[0, _] = _afp[_] o[0] = temp return o def calculate_dOutputs_dInputs(parameters, derafp, outputs, nsymbol,): """ Parameters ---------- parameters : dict ASE dictionary object. derafp : list Derivatives of atomic fingerprints in the form of a list to be used as input to the neural network. outputs : dict Outputs of neural network nodes. nsymbol : str Symbol of the atom for which atomic energy is calculated (only used in the atom-centered mode) Returns ------- dict Derivatives of outputs of neural network nodes w.r.t. inputs. """ _derafp = np.array(derafp).copy() hiddenlayers = parameters.hiddenlayers[nsymbol] weight = parameters.weights[nsymbol] activation = parameters.activation fprange = parameters.fprange[nsymbol] # Scaling derivative of fingerprints. for _ in range(len(_derafp)): if (fprange[_][1] - fprange[_][0]) > (10.**(-8.)): _derafp[_] = 2.0 * (_derafp[_] / (fprange[_][1] - fprange[_][0])) dOutputs_dInputs = {} # node values dOutputs_dInputs[0] = _derafp layer = 0 # input layer for hiddenlayer in hiddenlayers[0:]: layer += 1 temp = np.dot(np.matrix(dOutputs_dInputs[layer - 1]), np.delete(weight[layer], -1, 0)) dOutputs_dInputs[layer] = [None] * np.size(outputs[layer]) bound = np.size(outputs[layer]) for j in range(bound): if activation == 'linear': # linear function dOutputs_dInputs[layer][j] = float(temp[0, j]) elif activation == 'sigmoid': # sigmoid function dOutputs_dInputs[layer][j] = float(temp[0, j]) * \ float(outputs[layer][0, j] * (1. - outputs[layer][0, j])) elif activation == 'tanh': # tanh function dOutputs_dInputs[layer][j] = float(temp[0, j]) * \ float(1. - outputs[layer][0, j] * outputs[layer][0, j]) layer += 1 # output layer temp = np.dot(np.matrix(dOutputs_dInputs[layer - 1]), np.delete(weight[layer], -1, 0)) if activation == 'linear': # linear function dOutputs_dInputs[layer] = float(temp) elif activation == 'sigmoid': # sigmoid function dOutputs_dInputs[layer] = \ float(outputs[layer] * (1. - outputs[layer]) * temp) elif activation == 'tanh': # tanh function dOutputs_dInputs[layer] = \ float((1. - outputs[layer] * outputs[layer]) * temp) dOutputs_dInputs[layer] = [dOutputs_dInputs[layer]] return dOutputs_dInputs def calculate_ohat_D_delta(parameters, outputs, W): """Calculates extra matrices ohat, D, delta needed in mathematical manipulations. Notations are consistent with those of 'Rojas, R. Neural Networks - A Systematic Introduction. Springer-Verlag, Berlin, first edition 1996' Parameters ---------- parameters : dict ASE dictionary object. outputs : dict Outputs of neural network nodes. W : dict The same as weight dictionary, but the last rows associated with biases are deleted in W. """ activation = parameters.activation N = len(outputs) - 2 # number of hiddenlayers D = {} for k in range(N + 2): D[k] = np.zeros(shape=(np.size(outputs[k]), np.size(outputs[k]))) for j in range(np.size(outputs[k])): if activation == 'linear': # linear D[k][j, j] = 1. elif activation == 'sigmoid': # sigmoid D[k][j, j] = float(outputs[k][0, j]) * \ float((1. - outputs[k][0, j])) elif activation == 'tanh': # tanh D[k][j, j] = float(1. - outputs[k][0, j] * outputs[k][0, j]) # Calculating delta delta = {} # output layer delta[N + 1] = D[N + 1] # hidden layers for k in range(N, 0, -1): # backpropagate starting from output layer delta[k] = np.dot(D[k], np.dot(W[k + 1], delta[k + 1])) # Calculating ohat ohat = {} for k in range(1, N + 2): bound = np.size(outputs[k - 1]) ohat[k - 1] = np.zeros(shape=(1, bound + 1)) for j in range(bound): ohat[k - 1][0, j] = outputs[k - 1][0, j] ohat[k - 1][0, bound] = 1.0 return ohat, D, delta def get_random_weights(hiddenlayers, activation, no_of_atoms=None, fprange=None): """Generates random weight arrays from variables. hiddenlayers: dict Dictionary of chemical element symbols and architectures of their corresponding hidden layers of the conventional neural network. Number of nodes of last layer is always one corresponding to energy. However, number of nodes of first layer is equal to three times number of atoms in the system in the case of no descriptor, and is equal to length of symmetry functions in the atom-centered mode. Can be fed as: >>> hiddenlayers = (3, 2,) for example, in which a neural network with two hidden layers, the first one having three nodes and the second one having two nodes is assigned (to the whole atomic system in the case of no descriptor, and to each chemical element in the atom-centered mode). In the atom-centered mode, neural network for each species can be assigned seperately, as: >>> hiddenlayers = {"O":(3,5), "Au":(5,6)} for example. activation : str Assigns the type of activation funtion. "linear" refers to linear function, "tanh" refers to tanh function, and "sigmoid" refers to sigmoid function. no_of_atoms : int Number of atoms in atomic systems; used only in the case of no descriptor. fprange : dict Range of fingerprints of each chemical species. Should be fed as a dictionary of chemical species and a list of minimum and maximun, e.g: >>> fprange={"Pd": [0.31, 0.59], "O":[0.56, 0.72]} Returns ------- float weights """ weight = {} nn_structure = {} if no_of_atoms is not None: # pure atomic-coordinates scheme if isinstance(hiddenlayers, int): nn_structure = ([3 * no_of_atoms] + [hiddenlayers] + [1]) else: nn_structure = ( [3 * no_of_atoms] + [layer for layer in hiddenlayers] + [1]) weight = {} # Instead try Andrew Ng coursera approach. +/- epsilon # epsilon = sqrt(6./(n_i + n_o)) # where the n's are the number of input and output nodes. # Note: need to double that here with the math below. epsilon = np.sqrt(6. / (nn_structure[0] + nn_structure[1])) normalized_arg_range = 2. * epsilon weight[1] = np.random.random((3 * no_of_atoms + 1, nn_structure[1])) * \ normalized_arg_range - \ normalized_arg_range / 2. len_of_hiddenlayers = len(list(nn_structure)) - 3 for layer in range(len_of_hiddenlayers): epsilon = np.sqrt(6. / (nn_structure[layer + 1] + nn_structure[layer + 2])) normalized_arg_range = 2. * epsilon weight[layer + 2] = np.random.random( (nn_structure[layer + 1] + 1, nn_structure[layer + 2])) * \ normalized_arg_range - normalized_arg_range / 2. epsilon = np.sqrt(6. / (nn_structure[-2] + nn_structure[-1])) normalized_arg_range = 2. * epsilon weight[len(list(nn_structure)) - 1] = \ np.random.random((nn_structure[-2] + 1, 1)) \ * normalized_arg_range - normalized_arg_range / 2. if False: # This seemed to be setting all biases to zero? len_of_weight = len(weight) for _ in range(len_of_weight): # biases size = weight[_ + 1][-1].size for __ in range(size): weight[_ + 1][-1][__] = 0. else: elements = fprange.keys() for element in sorted(elements): len_of_fps = len(fprange[element]) if isinstance(hiddenlayers[element], int): nn_structure[element] = ([len_of_fps] + [hiddenlayers[element]] + [1]) else: nn_structure[element] = ( [len_of_fps] + [layer for layer in hiddenlayers[element]] + [1]) weight[element] = {} # Instead try Andrew Ng coursera approach. +/- epsilon # epsilon = sqrt(6./(n_i + n_o)) # where the n's are the number of input and output nodes. # Note: need to double that here with the math below. epsilon = np.sqrt(6. / (nn_structure[element][0] + nn_structure[element][1])) normalized_arg_range = 2. * epsilon weight[element][1] = np.random.random((len(fprange[element]) + 1, nn_structure[ element][1])) * \ normalized_arg_range - \ normalized_arg_range / 2. len_of_hiddenlayers = len(list(nn_structure[element])) - 3 for layer in range(len_of_hiddenlayers): epsilon = np.sqrt(6. / (nn_structure[element][layer + 1] + nn_structure[element][layer + 2])) normalized_arg_range = 2. * epsilon weight[element][layer + 2] = np.random.random( (nn_structure[element][layer + 1] + 1, nn_structure[element][layer + 2])) * \ normalized_arg_range - normalized_arg_range / 2. epsilon = np.sqrt(6. / (nn_structure[element][-2] + nn_structure[element][-1])) normalized_arg_range = 2. * epsilon weight[element][len(list(nn_structure[element])) - 1] = \ np.random.random((nn_structure[element][-2] + 1, 1)) \ * normalized_arg_range - normalized_arg_range / 2. if False: # This seemed to be setting all biases to zero? len_of_weight = len(weight[element]) for _ in range(len_of_weight): # biases size = weight[element][_ + 1][-1].size for __ in range(size): weight[element][_ + 1][-1][__] = 0. return weight def get_random_scalings(images, activation, elements=None): """Generates initial scaling matrices, such that the range of activation is scaled to the range of actual energies. images : dict ASE atoms objects (the training set). activation: str Assigns the type of activation funtion. "linear" refers to linear function, "tanh" refers to tanh function, and "sigmoid" refers to sigmoid function. elements: list of str List of atom symbols; used in the atom-centered mode only. Returns ------- float scalings """ hashs = list(images.keys()) no_of_images = len(hashs) max_act_energy = max(image.get_potential_energy(apply_constraint=False) for image in images.values()) min_act_energy = min(image.get_potential_energy(apply_constraint=False) for image in images.values()) for count in range(no_of_images): hash = hashs[count] image = images[hash] no_of_atoms = len(image) if image.get_potential_energy(apply_constraint=False) == \ max_act_energy: no_atoms_of_max_act_energy = no_of_atoms if image.get_potential_energy(apply_constraint=False) == \ min_act_energy: no_atoms_of_min_act_energy = no_of_atoms max_act_energy_per_atom = max_act_energy / no_atoms_of_max_act_energy min_act_energy_per_atom = min_act_energy / no_atoms_of_min_act_energy scaling = {} if elements is None: # pure atomic-coordinates scheme scaling = {} if activation == 'sigmoid': # sigmoid activation function scaling['intercept'] = min_act_energy_per_atom scaling['slope'] = (max_act_energy_per_atom - min_act_energy_per_atom) elif activation == 'tanh': # tanh activation function scaling['intercept'] = (max_act_energy_per_atom + min_act_energy_per_atom) / 2. scaling['slope'] = (max_act_energy_per_atom - min_act_energy_per_atom) / 2. elif activation == 'linear': # linear activation function scaling['intercept'] = (max_act_energy_per_atom + min_act_energy_per_atom) / 2. scaling['slope'] = (10. ** (-10.)) * \ (max_act_energy_per_atom - min_act_energy_per_atom) / 2. else: # atom-centered mode for element in elements: scaling[element] = {} if activation == 'sigmoid': # sigmoid activation function scaling[element]['intercept'] = min_act_energy_per_atom scaling[element]['slope'] = (max_act_energy_per_atom - min_act_energy_per_atom) elif activation == 'tanh': # tanh activation function scaling[element]['intercept'] = (max_act_energy_per_atom + min_act_energy_per_atom) / 2. scaling[element]['slope'] = (max_act_energy_per_atom - min_act_energy_per_atom) / 2. elif activation == 'linear': # linear activation function scaling[element]['intercept'] = (max_act_energy_per_atom + min_act_energy_per_atom) / 2. scaling[element]['slope'] = (10. ** (-10.)) * \ (max_act_energy_per_atom - min_act_energy_per_atom) / 2. return scaling class Raveler: """Class to ravel and unravel variable values into a single vector. This is used for feeding into the optimizer. Feed in a list of dictionaries to initialize the shape of the transformation. Note no data is saved in the class; each time it is used it is passed either the dictionaries or vector. The dictionaries for initialization should be two levels deep. weights, scalings are the variables to ravel and unravel """ def __init__(self, weights, scalings): self.count = 0 self.weightskeys = [] self.scalingskeys = [] for key1 in sorted(weights.keys()): # element for key2 in sorted(weights[key1].keys()): # layer value = weights[key1][key2] self.weightskeys.append({'key1': key1, 'key2': key2, 'shape': np.array(value).shape, 'size': np.array(value).size}) self.count += np.array(weights[key1][key2]).size for key1 in sorted(scalings.keys()): # element for key2 in sorted(scalings[key1].keys()): # slope / intercept self.scalingskeys.append({'key1': key1, 'key2': key2}) self.count += 1 self.vector = np.zeros(self.count) def to_vector(self, weights, scalings): """Puts the weights and scalings embedded dictionaries into a single vector and returns it. The dictionaries need to have the identical structure to those it was initialized with.""" vector = np.zeros(self.count) count = 0 for k in self.weightskeys: lweights = np.array(weights[k['key1']][k['key2']]).ravel() vector[count:(count + lweights.size)] = lweights count += lweights.size for k in self.scalingskeys: vector[count] = scalings[k['key1']][k['key2']] count += 1 return vector def to_dicts(self, vector): """Puts the vector back into weights and scalings dictionaries of the form initialized. vector must have same length as the output of unravel.""" assert len(vector) == self.count count = 0 weights = OrderedDict() scalings = OrderedDict() for k in self.weightskeys: if k['key1'] not in weights.keys(): weights[k['key1']] = OrderedDict() matrix = vector[count:count + k['size']] matrix = matrix.flatten() matrix = np.matrix(matrix.reshape(k['shape'])) weights[k['key1']][k['key2']] = matrix.tolist() count += k['size'] for k in self.scalingskeys: if k['key1'] not in scalings.keys(): scalings[k['key1']] = OrderedDict() scalings[k['key1']][k['key2']] = vector[count] count += 1 return weights, scalings # Analysis tools ############################################################## class NodePlot: """Creates plots to visualize the output of the nodes in the neural networks. initialize with a calculator that has parameters; e.g. a trained calculator or else one in which fit has been called with the setup_only flag turned on. Call with the 'plot' method, which takes as argment a list of images """ def __init__(self, calc): self.calc = calc self.data = {} # For accumulating the data. # Local imports; these are not package-wide dependencies. from matplotlib import pyplot from matplotlib.backends.backend_pdf import PdfPages self.pyplot = pyplot self.PdfPages = PdfPages def plot(self, images, filename='nodeplot.pdf'): """ Creates a plot of the output of each node, as a violin plot. """ calc = self.calc log = Logger('develop.log') images = hash_images(images, log=log) calc.descriptor.calculate_fingerprints(images=images, parallel={'cores': 1}, log=log, calculate_derivatives=False) for hash in images.keys(): fingerprints = calc.descriptor.fingerprints[hash] for fp in fingerprints: outputs = calculate_nodal_outputs(calc.model.parameters, afp=fp[1], symbol=fp[0]) self._accumulate(symbol=fp[0], output=outputs) self._finalize_table() with self.PdfPages(filename) as pdf: for symbol in self.data.keys(): fig = self._makefig(symbol) pdf.savefig(fig) self.pyplot.close(fig) def _makefig(self, symbol, save=False): """Makes a figure for one element.""" fig = self.pyplot.figure(figsize=(8.5, 11.0)) lm = 0.1 rm = 0.05 bm = 0.05 tm = 0.05 vg = 0.05 numplots = 1 + self.data[symbol]['header'][-1][0] axwidth = 1. - lm - rm axheight = (1. - bm - tm - (numplots - 1) * vg) / numplots d = self.data[symbol] for layer in range(1 + d['header'][-1][0]): ax = fig.add_axes((lm, 1. - tm - axheight - (axheight + vg) * layer, axwidth, axheight)) indices = [_ for _, label in enumerate(d['header']) if label[0] == layer] sub = d['table'][:, indices] ax.violinplot(dataset=sub, positions=range(len(indices))) ax.set_ylim(-1.2, 1.2) ax.set_xlim(-0.5, len(indices) - 0.5) ax.set_ylabel('Layer %i' % layer) ax.set_xlabel('node') fig.text(0.5, 1. - 0.5 * tm, 'Node outputs for %s' % symbol, ha='center', va='center') if save: fig.savefig(save) return fig def _accumulate(self, symbol, output): """Accumulates the data for the symbol.""" data = self.data layerkeys = list(output.keys()) # Correspond to layers. if symbol not in data: # Create headers, structure. data[symbol] = {'header': [], 'table': []} for layerkey in layerkeys: v = output[layerkey] v = v.reshape(v.size).tolist() data[symbol]['header'].extend([(layerkey, _) for _ in range(len(v))]) # Add as a row to data table. row = [] for layerkey in layerkeys: v = output[layerkey] v = v.reshape(v.size).tolist() row.extend(v) data[symbol]['table'].append(row) def _finalize_table(self): """Converts the data table into a numpy array.""" for symbol in self.data: self.data[symbol]['table'] = np.array(self.data[symbol]['table']) andrewpeterson-amp-4878fc892f2c/amp/model/tflow.py000066400000000000000000002356531332417112400220460ustar00rootroot00000000000000# This module was contributed by: # Zachary Ulissi # Department of Chemical Engineering # Stanford University # zulissi@gmail.com # Help/testing/discussions: Andrew Doyle (Stanford) and # the AMP development team # This module implements energy- and force- training using Google's # TensorFlow library. In doing so, the training is multithreaded and GPU # accelerated. import numpy as np import uuid from . import LossFunction from ..utilities import ConvergenceOccurred try: import tensorflow as tf from tensorflow.contrib.opt import ScipyOptimizerInterface except ImportError: # A warning is raised instead of an error so that documentation can # build without tensorflow installed. import warnings warnings.warn('Please install tensorflow if you plan to use this ' 'Amp module.') class NeuralNetwork: """TensorFlow-based Neural Network model. Uses Google's machine-learning code to construct a neural network. This method also allows for GPU acceleration. Parameters ---------- hiddenlayers Structure of the neural network. Can either be in the format (int,int,int), where each element represnts the size of a layer and there and the length of the list is the number of layers, or dictionary format of the network structure for each element type. E.g. {'Cu': (5, 5), 'O': (10, 5)} activation Activation type. (XXX Provide list of possibilities.) keep_prob : float Dropout rate for the neural network to reduce overfitting. (keep_prob=1. uses all nodes, keep_prob~0.5-0.8 better for training) maxTrainingEpochs : int Maximum number of times to loop through the training data before giving up. batchsize : int Batch size for minibatch (if miniBatch is set to True). initialTrainingRate Initial training rate for SGD optimizers like ADAM. See the TF documentation for choose this value. Likely between 1e-2 and 1e-5, depending on use case, whether mini-batch is on, etc. miniBatch : bool Whether to use minibatches in training. tfVars Tensorflow variables (used if restoring from a previous save). saveVariableName : str Name used for the internal tensorflow variable naming scheme. If variables have the same name as another model in the same tensorflow session, there will be collisions. parameters Dictionary of parameters to be used in initialization. Mostly these are the same keywords as the keyword arguments in this function. This is primarily used to make saving/loading easier. sess tensorflow session to use (None means start a new session) maxAtomsForces : int Number of atoms to be used in the force training. It sets the upper bound on the number of atoms that can be used to calculate the force for. E.g., if maxAtomsForces=40, then forces can only be calculated for images with less than 40 atoms. energy_coefficient : float Used to adjust the loss function; this is the weight applied to the energy component. force_coefficient : float or None Used to adjust the loss function; this is the weight applied to the force component. Note you can turn off force training by setting this to None. convergenceCriteria: dict Dictionary of convergence criteria, analagous to the main AMP convergence criteria dictionary. optimizationMethod: string Set the optimization method for the NN parameters. Currently either 'ADAM' for the ADAM optimizer in tensorflow, of 'l-BFGS-b' for the deterministic l-BFGS-b method. ADAM is usually faster per training step, has all of the benefits of being a stochastic optimizer, and allows for mini-batch operation, but has more tunable parameters and can be harder to get working well. l-BFGS-b usually works for small/moderate network sizes. input_keep_prob Dropout ratio on the first layer (from fingerprints to the neural network. Rule of thumb is this should be 0 to 0.2. Only applies when using a SGD optimizer like ADAM. BFGS ignores this. ADAM_optimizer_params Dictionary of parameters to pass to the ADAM optimizer. See https://www.tensorflow.org/versions/r0.11/api_docs/python/ train.html#AdamOptimizer for documentation regularization_strength Weight for L2-regularization in the cost function fprange: dict This is a dictionary that contains the minimum and maximum values seen for each fingerprint of each element. These weights: np array Input that allows the NN weights (and biases) to be set directly. This is only used for verifying that the calculation is working correctly in the CuOPd test case. In general, don't use this except for testing the code. This argument is analagous to the original AMP NeuralNetwork module. scalings Input that allows the NN final scaling o be set directly. This is only used for verifying that the calculation is working correctly in the CuOPd test case. In general, don't use this except for testing the code. This argument is analagous to the original AMP NeuralNetwork module. unit_type: string Sets the internal datatype of the tensorflow model. Either "float" for 32-bit FP precision, or "double" for 64-bit FP precision. preLoadTrainingData: bool Decides whether to run the training by preloading all training data into tensorflow. Doing so results in faster training if the entire dataset can fit into memory. This only works when not using mini-batch. relativeForceCutoff: float Parameter for controlling whether the force contribution to the trained cost function is absolute (just differences of force compared to training forces) or relative for large values of the force. This basically sets the upper limit on the forces that should be fitted (e.g. if the force is >A, then the force is scaled). This helps when a small number of images have very large forces that don't need to be reconstructed perfectly. """ def __init__(self, hiddenlayers=(5, 5), activation='tanh', keep_prob=1., maxTrainingEpochs=10000, importname=None, batchsize=2, initialTrainingRate=1e-4, miniBatch=False, tfVars=None, saveVariableName=None, parameters=None, sess=None, energy_coefficient=1.0, force_coefficient=0.04, scikit_model=None, convergenceCriteria=None, optimizationMethod='l-BFGS-b', input_keep_prob=0.8, ADAM_optimizer_params={'beta1': 0.9}, regularization_strength=None, numTrainingImages={}, elementFingerprintLengths=None, fprange=None, weights=None, scalings=None, unit_type="float", preLoadTrainingData=True, relativeForceCutoff=None ): self.parameters = {} if parameters is None else parameters for prop in ['energyMeanScale', 'energyPerElement']: if prop not in self.parameters: self.parameters[prop] = 0. for prop in ['energyProdScale']: if prop not in self.parameters: self.parameters[prop] = 1. if 'convergence' in self.parameters: 1 elif convergenceCriteria is None: self.parameters['convergence'] = {'energy_rmse': 0.001, 'energy_maxresid': None, 'force_rmse': 0.005, 'force_maxresid': None} else: self.parameters['convergence'] = convergenceCriteria if 'energy_coefficient' not in self.parameters: self.parameters['energy_coefficient'] = energy_coefficient if 'force_coefficient' not in self.parameters: self.parameters['force_coefficient'] = force_coefficient if 'ADAM_optimizer_params' not in self.parameters: self.parameters['ADAM_optimizer_params'] = ADAM_optimizer_params if 'regularization_strength' not in self.parameters: self.parameters['regularization_strength'] =\ regularization_strength if 'relativeForceCutoff' not in self.parameters: self.parameters['relativeForceCutoff'] = relativeForceCutoff if 'unit_type' not in self.parameters: self.parameters['unit_type'] = unit_type if 'preLoadTrainingData' not in self.parameters: self.parameters['preLoadTrainingData'] = preLoadTrainingData if 'fprange' not in self.parameters and fprange is not None: self.parameters['fprange'] = {} for element in fprange: _ = np.array([map(lambda x: x[0], fprange[element]), map(lambda x: x[1], fprange[element])]) self.parameters['fprange'][element] = _ self.hiddenlayers = hiddenlayers if isinstance(activation, basestring): self.activationName = activation self.activation = eval('tf.nn.' + activation) else: self.activation = activation self.activationName = activation.__name__ self.keep_prob = keep_prob self.input_keep_prob = input_keep_prob if saveVariableName is None: self.saveVariableName = str(uuid.uuid4())[:8] else: self.saveVariableName = saveVariableName if elementFingerprintLengths is not None: self.elements = elementFingerprintLengths.keys() self.elements.sort() self.elementFingerprintLengths = {} for element in self.elements: self.elementFingerprintLengths[element] =\ elementFingerprintLengths[element] self.weights = weights self.scalings = scalings self.sess = sess self.graph = None if tfVars is not None: self.constructSessGraphModel(tfVars, self.sess) if weights is not None: self.elementFingerprintLengths = {} self.elements = weights.keys() for element in self.elements: self.elementFingerprintLengths[element] =\ weights[element][1].shape[0] - 1 self.constructSessGraphModel(tfVars, self.sess) self.tfVars = tfVars self.maxTrainingEpochs = maxTrainingEpochs self.importname = '.model.neuralnetwork.tflow' self.batchsize = batchsize self.initialTrainingRate = initialTrainingRate self.miniBatch = miniBatch # Optimizer can be 'ADAM' or 'l-BFGS-b'. self.optimizationMethod = optimizationMethod # self.forcetraining is queried by the main Amp instance. if self.parameters['force_coefficient'] is None: self.forcetraining = False self.parameters['convergence']['force_rmse'] = None self.parameters['convergence']['force_maxresid'] = None else: self.forcetraining = True def constructSessGraphModel(self, tfVars, sess, trainOnly=False, numElements=None, numTrainingImages=None, num_dgdx_Eindices=None, numTrainingAtoms=None): self.graph = tf.Graph() with self.graph.as_default(): if sess is None: self.sess = tf.InteractiveSession() else: self.sess = sess if trainOnly: self.constructModel(self.sess, self.graph, trainOnly, numElements, numTrainingImages, num_dgdx_Eindices, numTrainingAtoms) else: self.constructModel(self.sess, self.graph) trainvarlist = tf.trainable_variables() trainvarlist = [a for a in trainvarlist if a.name[:8] == self.saveVariableName] self.saver = tf.train.Saver(trainvarlist) if tfVars is not None: self.sess.run(tf.initialize_all_variables()) with open('tfAmpNN-checkpoint-restore', 'w') as fhandle: fhandle.write(tfVars) self.saver.restore(self.sess, 'tfAmpNN-checkpoint-restore') else: self.sess.run(tf.initialize_all_variables()) # This function is used to test the code by pre-setting the weights in the # model for each element, so that results can be checked against # pre-computed exact estimates def setWeightsScalings(self, feedinput, weights, scalings): with self.graph.as_default(): namefun = lambda x: '%s_%s_' % (self.saveVariableName, element) + x for element in weights: for layer in weights[element]: weight = weights[element][layer][0:-1] bias = weights[element][layer][-1] bias = np.array(bias).reshape(bias.size) feedinput[self.graph.get_tensor_by_name( namefun('Wfc%d:0' % (layer - 1)))] = weight feedinput[self.graph.get_tensor_by_name( namefun('bfc%d:0' % (layer - 1)))] = bias feedinput[ self.graph.get_tensor_by_name(namefun('Wfcout:0'))] = \ np.array(scalings[element]['slope']).reshape((1, 1)) feedinput[ self.graph.get_tensor_by_name(namefun('bfcout:0'))] = \ np.array(scalings[element]['intercept']).reshape((1,)) def constructModel(self, sess, graph, preLoadData=False, numElements=None, numTrainingImages=None, num_dgdx_Eindices=None, numTrainingAtoms=None): """Sets up the tensorflow neural networks for each atom type.""" with sess.as_default(), graph.as_default(): # Make tensorflow inputs for each element. tensordict = {} indsdict = {} maskdict = {} dgdx_dict = {} dgdx_Eindices_dict = {} dgdx_Xindices_dict = {} if preLoadData: tensordictInitializer = {} dgdx_dict_initializer = {} dgdx_Eindices_dict_initializer = {} dgdx_Xindices_dict_initializer = {} indsdictInitializer = {} maskdictInitializer = {} for element in self.elements: if preLoadData: tensordictInitializer[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[numElements[element], self.elementFingerprintLengths[ element]], name='tensor_%s' % element,) dgdx_dict_initializer[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[num_dgdx_Eindices[element], self.elementFingerprintLengths[ element], 3], name='dgdx_%s' % element,) dgdx_Eindices_dict_initializer[element] = \ tf.placeholder("int64", shape=[num_dgdx_Eindices[element]], name='dgdx_Eindices_%s' % element,) dgdx_Xindices_dict_initializer[element] = \ tf.placeholder("int64", shape=[num_dgdx_Eindices[element]], name='dgdx_Xindices_%s' % element,) indsdictInitializer[element] = \ tf.placeholder("int64", shape=[numElements[element]], name='indsdict_%s' % element,) maskdictInitializer[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[numTrainingImages, 1], name='maskdict_%s' % element,) tensordict[element] = \ tf.Variable(tensordictInitializer[element], trainable=False, collections=[],) dgdx_dict[element] = \ tf.Variable(dgdx_dict_initializer[element], trainable=False, collections=[],) dgdx_Eindices_dict[element] = \ tf.Variable(dgdx_Eindices_dict_initializer[element], trainable=False, collections=[],) dgdx_Xindices_dict[element] = \ tf.Variable(dgdx_Xindices_dict_initializer[element], trainable=False, collections=[]) indsdict[element] = \ tf.Variable(indsdictInitializer[element], trainable=False, collections=[]) maskdict[element] = \ tf.Variable(maskdictInitializer[element], trainable=False, collections=[]) else: tensordict[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[None, self.elementFingerprintLengths[ element]], name='tensor_%s' % element,) dgdx_dict[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[None, self.elementFingerprintLengths[ element], 3], name='dgdx_%s' % element) dgdx_Eindices_dict[element] = \ tf.placeholder("int64", shape=[None], name='dgdx_Eindices_%s' % element) dgdx_Xindices_dict[element] = \ tf.placeholder("int64", shape=[None], name='dgdx_Xindices_%s' % element) indsdict[element] = \ tf.placeholder("int64", shape=[None], name='indsdict_%s' % element) maskdict[element] = \ tf.placeholder(self.parameters['unit_type'], shape=[None, 1], name='maskdict_%s' % element) self.indsdict = indsdict self.tensordict = tensordict self.maskdict = maskdict self.dgdx_dict = dgdx_dict self.dgdx_Eindices_dict = dgdx_Eindices_dict self.dgdx_Xindices_dict = dgdx_Xindices_dict # y_ is the input energy for each configuration. if preLoadData: y_Initializer = \ tf.placeholder(self.parameters['unit_type'], shape=[numTrainingImages, 1], name='y_') input_keep_prob_inInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[], name='input_keep_prob_in') keep_prob_inInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[], name='keep_prob_in') nAtoms_inInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[numTrainingImages, 1], name='nAtoms_in') nAtoms_forces_Initializer = \ tf.placeholder(self.parameters['unit_type'], shape=[numTrainingAtoms, 1], name='nAtoms_forces') batchsizeInputInitializer = \ tf.placeholder("int32", shape=[], name='batchsizeInput') learningrateInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[], name='learningrate') forces_inInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[numTrainingAtoms, 3], name='forces_in') energycoefficientInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[]) forcecoefficientInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[]) energyProdScaleInitializer = \ tf.placeholder(self.parameters['unit_type'], shape=[], name='energyProdScale') totalNumAtomsInitializer = \ tf.placeholder("int32", shape=[], name='totalNumAtoms') self.y_ = \ tf.Variable(y_Initializer, trainable=False, collections=[]) self.input_keep_prob_in = \ tf.Variable(input_keep_prob_inInitializer, trainable=False, collections=[]) self.keep_prob_in = \ tf.Variable(keep_prob_inInitializer, trainable=False, collections=[]) self.nAtoms_in = \ tf.Variable(nAtoms_inInitializer, trainable=False, collections=[]) self.batchsizeInput = \ tf.Variable(batchsizeInputInitializer, trainable=False, collections=[]) self.learningrate = \ tf.Variable(learningrateInitializer, trainable=False, collections=[]) self.forces_in = \ tf.Variable(forces_inInitializer, trainable=False, collections=[]) self.energycoefficient = \ tf.Variable(energycoefficientInitializer, trainable=False, collections=[]) self.forcecoefficient = \ tf.Variable(forcecoefficientInitializer, trainable=False, collections=[]) self.energyProdScale = \ tf.Variable(energyProdScaleInitializer, trainable=False, collections=[]) self.totalNumAtoms = \ tf.Variable(totalNumAtomsInitializer, trainable=False, collections=[]) self.nAtoms_forces = \ tf.Variable(nAtoms_forces_Initializer, trainable=False, collections=[]) self.initializers = \ {'indsdict': indsdictInitializer, 'dgdx_dict': dgdx_dict_initializer, 'dgdx_Xindices_dict': dgdx_Xindices_dict_initializer, 'dgdx_Eindices_dict': dgdx_Eindices_dict_initializer, 'maskdict': maskdictInitializer, 'tensordict': tensordictInitializer, 'y_': y_Initializer, 'input_keep_prob_in': input_keep_prob_inInitializer, 'keep_prob_in': keep_prob_inInitializer, 'nAtoms_in': nAtoms_inInitializer, 'batchsizeInput': batchsizeInputInitializer, 'learningrate': learningrateInitializer, 'forces_in': forces_inInitializer, 'energycoefficient': energycoefficientInitializer, 'forcecoefficient': forcecoefficientInitializer, 'energyProdScale': energyProdScaleInitializer, 'totalNumAtoms': totalNumAtomsInitializer, 'nAtoms_forces': nAtoms_forces_Initializer} else: self.y_ = \ tf.placeholder(self.parameters['unit_type'], shape=[None, 1], name='y_') self.input_keep_prob_in = \ tf.placeholder(self.parameters['unit_type'], name='input_keep_prob_in') self.keep_prob_in = \ tf.placeholder(self.parameters['unit_type'], name='keep_prob_in') self.nAtoms_in = \ tf.placeholder(self.parameters['unit_type'], shape=[None, 1], name='nAtoms_in') self.batchsizeInput = \ tf.placeholder("int32", name='batchsizeInput') self.learningrate = \ tf.placeholder(self.parameters['unit_type'], name='learningrate') self.forces_in = \ tf.placeholder(self.parameters['unit_type'], shape=[None, None, 3], name='forces_in') self.energycoefficient = \ tf.placeholder(self.parameters['unit_type']) self.forcecoefficient = \ tf.placeholder(self.parameters['unit_type']) self.energyProdScale = \ tf.placeholder(self.parameters['unit_type'], name='energyProdScale') self.totalNumAtoms = \ tf.placeholder("int32", name='totalNumAtoms') self.nAtoms_forces = \ tf.placeholder(self.parameters['unit_type'], shape=[None, 1], name='totalNumAtoms') # Generate a multilayer neural network for each element type. outdict = {} forcedict = {} l2_regularization_dict = {} for element in self.elements: if isinstance(self.hiddenlayers, dict): networkListToUse = self.hiddenlayers[element] else: networkListToUse = self.hiddenlayers (outdict[element], forcedict[element], l2_regularization_dict[element]) = \ model(tensordict[element], indsdict[element], self.keep_prob_in, self.input_keep_prob_in, self.batchsizeInput, networkListToUse, self.activation, self.elementFingerprintLengths[ element], mask=maskdict[ element], name=self.saveVariableName, dgdx=self.dgdx_dict[ element], dgdx_Eindices=self.dgdx_Eindices_dict[ element], dgdx_Xindices=self.dgdx_Xindices_dict[ element], element=element, unit_type=self.parameters[ 'unit_type'], totalNumAtoms=self.totalNumAtoms) self.outdict = outdict # The total energy is the sum of the energies over each atom type. keylist = self.elements ytot = outdict[keylist[0]] for i in range(1, len(keylist)): ytot = ytot + outdict[keylist[i]] self.energy = ytot * self.energyProdScale # The total force is the sum of the forces over each atom type. Ftot = forcedict[keylist[0]] for i in range(1, len(keylist)): Ftot = Ftot + forcedict[keylist[i]] self.forcedict = forcedict self.forces = -Ftot * self.energyProdScale l2_regularization = l2_regularization_dict[keylist[0]] for i in range(1, len(keylist)): l2_regularization = l2_regularization + \ l2_regularization_dict[keylist[i]] # Define output nodes for the energy of a configuration, a loss # function, and the loss per atom (which is what we usually track) # self.loss = tf.sqrt(tf.reduce_sum( # tf.square(tf.sub(self.energy, self.y_)))) # self.lossPerAtom = tf.reduce_sum( # tf.square(tf.div(tf.sub(self.energy, self.y_), self.nAtoms_in))) # loss function, as included in model/__init__.py self.energy_loss = tf.reduce_sum( tf.square(tf.div(tf.sub(self.energy, self.y_), self.nAtoms_in))) # Define the training step for energy training. # self.loss_forces = self.forcecoefficient * \ # tf.sqrt(tf.reduce_mean(tf.square(tf.sub(self.forces_in, # self.forces)))) # force loss function, as included in model/__init__.py if self.parameters['relativeForceCutoff'] is None: self.force_loss = tf.reduce_sum( tf.div(tf.square(tf.sub(self.forces_in, self.forces)), self.nAtoms_forces)) / 3. # tf.reduce_sum(tf.div( # tf.reduce_mean(tf.square(tf.sub(self.forces_in, # self.forces)), 2), self.nAtoms_in)) else: relativeA = self.parameters['relativeForceCutoff'] self.force_loss = \ tf.reduce_sum(tf.div(tf.div( tf.square( tf.sub( self.forces_in, self.forces)), tf.square( self.forces_in) + relativeA**2.) * relativeA**2., self.nAtoms_forces)) / 3. # tf.reduce_sum(tf.div(tf.reduce_mean( # tf.div(tf.square(tf.sub(self.forces_in, self.forces)), # tf.square(self.forces_in)+relativeA**2.)*relativeA**2.,2), # self.nAtoms_in)) # Define max residuals self.energy_maxresid = tf.reduce_max( tf.abs(tf.div(tf.sub(self.energy, self.y_), self.nAtoms_in))) self.force_maxresid = tf.reduce_max( tf.abs(tf.sub(self.forces_in, self.forces))) # Define the training step for force training. if self.parameters['regularization_strength'] is not None: self.loss = self.forcecoefficient * self.force_loss + \ self.energycoefficient * self.energy_loss + \ self.parameters[ 'regularization_strength'] * l2_regularization self.energy_loss_regularized = self.energy_loss + \ self.parameters[ 'regularization_strength'] * l2_regularization else: self.loss = self.forcecoefficient * self.force_loss + \ self.energycoefficient * self.energy_loss self.energy_loss_regularized = self.energy_loss self.adam_optimizer_instance = \ tf.train.AdamOptimizer(self.learningrate, **self.parameters[ 'ADAM_optimizer_params']) self.train_step = \ self.adam_optimizer_instance.minimize( self.energy_loss_regularized) self.train_step_forces = \ self.adam_optimizer_instance.minimize(self.loss) # self.loss_forces_relative = \ # self.forcecoefficient * \ # tf.sqrt(tf.reduce_mean(tf.square(tf.div(tf.sub(self.forces_in, # self.forces),self.forces_in+0.0001)))) # self.force_loss_relative = \ # tf.reduce_sum(tf.div(tf.reduce_mean( # tf.div(tf.square(tf.sub(self.forces_in, # self.forces)),tf.square(self.forces_in)+0.005**2.),2), # self.nAtoms_in)) # self.loss_relative = \ # self.forcecoefficient*self.loss_forces_relative + \ # self.energycoefficient*self.energy_loss # self.train_step_forces = # tf.adam_optimizer_instance.minimize(self.loss_relative) def initializeVariables(self): """Resets all of the variables in the current tensorflow model.""" self.sess.run(tf.initialize_all_variables()) def generateFeedInput(self, curinds, energies, atomArraysAll, dgdx, dgdx_Eindices, dgdx_Xindices, nAtomsDict, atomsIndsReverse, batchsize, trainingrate, keepprob, inputkeepprob, natoms, forcesExp=0., forces=False, energycoefficient=1., forcecoefficient=None, training=True): """Generates the input dictionary that maps various inputs on the python side to placeholders for the tensorflow model.""" (atomArraysFinal, dgdx_batch, dgdx_Eindices_batch, dgdx_Xindices_batch, atomInds) = \ generateBatch(curinds, self.elements, atomArraysAll, nAtomsDict, atomsIndsReverse, dgdx, dgdx_Eindices, dgdx_Xindices) feedinput = {} for element in self.elements: if len(atomArraysFinal[element]) > 0: aAF = atomArraysFinal[element].copy() for i in range(len(aAF)): for j in range(len(aAF[i])): if (self.parameters['fprange'][element][1][j] - self.parameters['fprange'][element][0][j]) > 10.**-8: aAF[i][j] = -1. + \ 2. * (atomArraysFinal[element][i][j] - self.parameters['fprange'][element][0][j]) / ( self.parameters['fprange'][element][1][j] - self.parameters['fprange'][element][0][j]) feedinput[self.tensordict[element]] = aAF feedinput[self.indsdict[element]] = atomInds[element] feedinput[self.maskdict[element]] = np.ones((batchsize, 1)) if forcecoefficient > 1.e-5: dgdx_to_scale = dgdx_batch[element] for i in range(dgdx_to_scale.shape[0]): for l in range(dgdx_to_scale.shape[1]): if (self.parameters['fprange'][element][1][l] - self.parameters['fprange'][element][0][l]) > 10.**-8: dgdx_to_scale[i][l][:] = \ 2. * dgdx_to_scale[i][l][:] / \ (self.parameters['fprange'][element][1][l] - self.parameters['fprange'][element][0][l]) feedinput[self.dgdx_dict[element]] = dgdx_to_scale feedinput[self.dgdx_Eindices_dict[ element]] = dgdx_Eindices_batch[element] feedinput[self.dgdx_Xindices_dict[ element]] = dgdx_Xindices_batch[element] else: feedinput[self.dgdx_dict[element]] = \ np.zeros((len(dgdx_Eindices[element]), self.elementFingerprintLengths[element], 3)) feedinput[self.dgdx_Eindices_dict[element]] = [] feedinput[self.dgdx_Xindices_dict[element]] = [] else: feedinput[self.tensordict[element]] = np.zeros( (1, self.elementFingerprintLengths[element])) feedinput[self.indsdict[element]] = [0] feedinput[self.maskdict[element]] = np.zeros((batchsize, 1)) feedinput[self.dgdx_dict[element]] = \ np.zeros((len(dgdx_Eindices[element]), self.elementFingerprintLengths[element], 3)) feedinput[self.dgdx_Eindices_dict[element]] = [] feedinput[self.dgdx_Xindices_dict[element]] = [] feedinput[self.batchsizeInput] = batchsize feedinput[self.learningrate] = trainingrate feedinput[self.keep_prob_in] = keepprob feedinput[self.input_keep_prob_in] = inputkeepprob natoms_forces = [] for natom in natoms[curinds]: for i in range(natom): natoms_forces.append(natom) natoms_forces = np.array(natoms_forces) feedinput[self.nAtoms_forces] = natoms_forces feedinput[self.nAtoms_in] = natoms[curinds] feedinput[self.totalNumAtoms] = np.sum(natoms[curinds]) if training: feedinput[self.y_] = energies[curinds] if forcecoefficient > 1.e-5: feedinput[self.forces_in] = np.concatenate( forcesExp[curinds], axis=0) feedinput[self.forcecoefficient] = forcecoefficient feedinput[self.energycoefficient] = energycoefficient feedinput[self.energyProdScale] = self.parameters['energyProdScale'] return feedinput def fit(self, trainingimages, descriptor, parallel, log=None): """Fit takes a bunch of training images (which are assumed to have a working calculator attached), and fits the internal variables to the training images. """ # if self.graph is None, the module hasn't been initialized if self.graph is None: self.elementFingerprintLengths = {} for element in descriptor.parameters.Gs: self.elementFingerprintLengths[element] = len( descriptor.parameters.Gs[element]) self.elements = self.elementFingerprintLengths.keys() self.elements.sort() self.constructSessGraphModel(self.tfVars, self.sess) self.log = log params = self.parameters lf = LossFunction(convergence=params['convergence'], energy_coefficient=params['energy_coefficient'], force_coefficient=params['force_coefficient'], parallel={'cores': 1}) if params['force_coefficient'] is not None: lf.attach_model(self, images=trainingimages, fingerprints=descriptor.fingerprints, fingerprintprimes=descriptor.fingerprintprimes) else: lf.attach_model(self, images=trainingimages, fingerprints=descriptor.fingerprints) lf._initialize() # Inputs: # trainingimages: batchsize = self.batchsize if self.parameters['force_coefficient'] is None: fingerprintDerDB = None else: fingerprintDerDB = descriptor.fingerprintprimes images = trainingimages keylist = images.keys() fingerprintDB = descriptor.fingerprints self.parameters['numTrainingImages'] = len(keylist) (atomArraysAll, nAtomsDict, atomsIndsReverse, natoms, dgdx, dgdx_Eindices, dgdx_Xindices) = \ generateTensorFlowArrays(fingerprintDB, self.elements, keylist, fingerprintDerDB) energies = map( lambda x: [images[x].get_potential_energy(apply_constraint=False)], keylist) energies = np.array(energies) if self.parameters['preLoadTrainingData'] and not(self.miniBatch): numElements = {} for element in nAtomsDict: numElements[element] = sum(nAtomsDict[element]) self.saver.save(self.sess, 'tfAmpNN-checkpoint') with open('tfAmpNN-checkpoint') as fhandle: tfvars = fhandle.read() self.sess.close() numTrainingAtoms = np.sum(map(lambda x: len(images[x]), keylist)) num_dgdx_Eindices = {} num_dgdx_Xindices = {} for element in self.elements: num_dgdx_Eindices[element] = sum( map(len, dgdx_Eindices[element])) num_dgdx_Xindices[element] = sum( map(len, dgdx_Xindices[element])) self.constructSessGraphModel(tfvars, None, trainOnly=True, numElements=numElements, numTrainingImages=len(keylist), num_dgdx_Eindices=num_dgdx_Eindices, numTrainingAtoms=numTrainingAtoms) natomsArray = np.zeros((len(keylist), len(self.elements))) for i in range(len(images)): for j in range(len(self.elements)): natomsArray[i][j] = nAtomsDict[self.elements[j]][i] (atomArraysAll, nAtomsDict, atomsIndsReverse, natoms, dgdx, dgdx_Eindices, dgdx_Xindices) = generateTensorFlowArrays(fingerprintDB, self.elements, keylist, fingerprintDerDB) self.parameters['energyMeanScale'] = np.mean(energies) energies = energies - self.parameters['energyMeanScale'] self.parameters['energyProdScale'] = np.mean(np.abs(energies)) self.parameters['fprange'] = {} for element in self.elements: if len(atomArraysAll[element]) == 0: self.parameters['fprange'][element] = [] else: self.parameters['fprange'][element] = \ [np.min(atomArraysAll[element], axis=0), np.max(atomArraysAll[element], axis=0)] if self.parameters['force_coefficient'] is not None: # forces = map(lambda x: images[x].get_forces( # apply_constraint=False), keylist) # forces = np.zeros((len(keylist), self.maxAtomsForces, 3)) forces = [] for i in range(len(keylist)): atoms = images[keylist[i]] forces.append(atoms.get_forces(apply_constraint=False)) forces = np.array(forces) else: forces = 0. if not(self.miniBatch): batchsize = len(keylist) def trainmodel(trainingrate, keepprob, inputkeepprob, maxepochs): icount = 1 icount_global = 1 indlist = np.arange(len(keylist)) converge_save = [] # continue taking training steps as long as we haven't hit the RMSE # minimum of the max number of epochs while (icount < maxepochs): # if we're in minibatch mode, shuffle the index list if self.miniBatch: np.random.shuffle(indlist) for i in range(int(len(keylist) / batchsize)): # if we're doing minibatch, construct a new set of inputs if self.miniBatch or (not(self.miniBatch)and(icount == 1)): if self.miniBatch: curinds = indlist[ np.arange(batchsize) + i * batchsize] else: curinds = range(len(keylist)) feedinput = self.generateFeedInput( curinds, energies, atomArraysAll, dgdx, dgdx_Eindices, dgdx_Xindices, nAtomsDict, atomsIndsReverse, batchsize, trainingrate, keepprob, inputkeepprob, natoms, forcesExp=forces, energycoefficient=self.parameters[ 'energy_coefficient'], forcecoefficient=self.parameters[ 'force_coefficient']) if (self.parameters['preLoadTrainingData'] and not(self.miniBatch)): self.preLoadFeed(feedinput) # run a training step with the inputs. if self.parameters['force_coefficient'] is None: self.sess.run(self.train_step, feed_dict=feedinput) else: self.sess.run(self.train_step_forces, feed_dict=feedinput) # Print the loss function every 100 evals. # if (self.miniBatch)and(icount % 100 == 0): # feed_keepprob_save=feedinput[self.keep_prob_in] # feed_keepprob_save_input=\ # feedinput[self.input_keep_prob_in] # feedinput[self.keep_prob_in]=1. # feedinput[self.keep_prob_in]=feed_keepprob_save icount += 1 # Every 10 epochs, report the RMSE on the entire training set if icount_global % 10 == 0: if self.miniBatch: feedinput = self.generateFeedInput( range(len(keylist)), energies, atomArraysAll, dgdx, dgdx_Eindices, dgdx_Xindices, nAtomsDict, atomsIndsReverse, len(keylist), trainingrate, 1., 1., natoms, forcesExp=forces, energycoefficient=self.parameters[ 'energy_coefficient'], forcecoefficient=self.parameters[ 'force_coefficient'], ) feed_keepprob_save = feedinput[self.keep_prob_in] feed_keepprob_save_input = feedinput[ self.input_keep_prob_in] feedinput[self.keep_prob_in] = 1. feedinput[self.input_keep_prob_in] = 1. if self.parameters['force_coefficient'] is not None: converge_save.append( [self.sess.run(self.loss, feed_dict=feedinput), self.sess.run( self.energy_loss, feed_dict=feedinput), self.sess.run( self.force_loss, feed_dict=feedinput), self.sess.run( self.energy_maxresid, feed_dict=feedinput), self.sess.run(self.force_maxresid, feed_dict=feedinput)]) if len(converge_save) > 2: converge_save.pop(0) convergence_vals = np.mean(converge_save, 0) converged = lf.check_convergence(*convergence_vals) if converged: raise ConvergenceOccurred() else: converged = \ lf.check_convergence( self.sess.run(self.energy_loss, feed_dict=feedinput), self.sess.run(self.energy_loss, feed_dict=feedinput), 0., self.sess.run(self.energy_maxresid, feed_dict=feedinput), 0.) if converged: raise ConvergenceOccurred() feedinput[self.keep_prob_in] = keepprob feedinput[self.input_keep_prob_in] = inputkeepprob icount_global += 1 return def trainmodelBFGS(maxEpochs): curinds = range(len(keylist)) feedinput = self.generateFeedInput( curinds, energies, atomArraysAll, dgdx, dgdx_Eindices, dgdx_Xindices, nAtomsDict, atomsIndsReverse, batchsize, 1., 1., 1., natoms, forcesExp=forces, energycoefficient=self.parameters[ 'energy_coefficient'], forcecoefficient=self.parameters['force_coefficient']) def step_callbackfun_forces(x): evalvarlist = map(lambda y: float(np.array(y(x))), varlist) converged = lf.check_convergence(*evalvarlist) if converged: raise ConvergenceOccurred() def step_callbackfun_noforces(x): converged = \ lf.check_convergence(float(np.array(varlist[1](x))), float(np.array(varlist[1](x))), 0., float(np.array(varlist[3](x))), 0.) if converged: raise ConvergenceOccurred() if self.parameters['force_coefficient'] is None: step_callbackfun = step_callbackfun_noforces curloss = self.energy_loss else: step_callbackfun = step_callbackfun_forces curloss = self.loss if self.parameters['preLoadTrainingData'] and not(self.miniBatch): self.preLoadFeed(feedinput) extOpt = \ ScipyOptimizerInterface(curloss, method='l-BFGS-b', options={'maxiter': maxEpochs, 'ftol': 1.e-10, 'gtol': 1.e-10, 'factr': 1.e4}) varlist = [] for var in [self.loss, self.energy_loss, self.force_loss, self.energy_maxresid, self.force_maxresid]: if (self.parameters['preLoadTrainingData'] and (not self.miniBatch)): varlist.append( extOpt._make_eval_func(var, self.sess, {}, [])) else: varlist.append(extOpt._make_eval_func(var, self.sess, feedinput, [])) extOpt.minimize(self.sess, feed_dict=feedinput, step_callback=step_callbackfun) return try: if self.optimizationMethod == 'l-BFGS-b': with self.graph.as_default(): trainmodelBFGS(self.maxTrainingEpochs) elif self.optimizationMethod == 'ADAM': trainmodel(self.initialTrainingRate, self.keep_prob, self.input_keep_prob, self.maxTrainingEpochs) else: log('uknown optimizer!') except ConvergenceOccurred: if self.parameters['preLoadTrainingData'] and not(self.miniBatch): self.saver.save(self.sess, 'tfAmpNN-checkpoint') with open('tfAmpNN-checkpoint') as fhandle: tfvars = fhandle.read() self.constructSessGraphModel(tfvars, None, trainOnly=False) return True return False def preLoadFeed(self, feedinput): for element in self.dgdx_dict: if self.dgdx_dict[element] in feedinput: self.sess.run(self.dgdx_dict[element].initializer, feed_dict={ self.initializers['dgdx_dict'][element]: feedinput[self.dgdx_dict[element]]}) self.sess.run(self.dgdx_Eindices_dict[element].initializer, feed_dict={ self.initializers['dgdx_Eindices_dict'][element]: feedinput[self.dgdx_Eindices_dict[element]]}) self.sess.run(self.dgdx_Xindices_dict[element].initializer, feed_dict={ self.initializers['dgdx_Xindices_dict'][element]: feedinput[self.dgdx_Xindices_dict[element]]}) del feedinput[self.dgdx_dict[element]] del feedinput[self.dgdx_Eindices_dict[element]] del feedinput[self.dgdx_Xindices_dict[element]] self.sess.run(self.tensordict[element].initializer, feed_dict={ self.initializers['tensordict'][element]: feedinput[self.tensordict[element]]}) self.sess.run(self.indsdict[element].initializer, feed_dict={ self.initializers['indsdict'][element]: feedinput[self.indsdict[element]]}) self.sess.run(self.maskdict[element].initializer, feed_dict={ self.initializers['maskdict'][element]: feedinput[self.maskdict[element]]}) del feedinput[self.tensordict[element]] del feedinput[self.indsdict[element]] del feedinput[self.maskdict[element]] self.sess.run(self.y_.initializer, feed_dict={ self.initializers['y_']: feedinput[self.y_]}) self.sess.run(self.input_keep_prob_in.initializer, feed_dict={ self.initializers['input_keep_prob_in']: feedinput[self.input_keep_prob_in]}) self.sess.run(self.keep_prob_in.initializer, feed_dict={ self.initializers['keep_prob_in']: feedinput[self.keep_prob_in]}) self.sess.run(self.nAtoms_in.initializer, feed_dict={ self.initializers['nAtoms_in']: feedinput[self.nAtoms_in]}) self.sess.run(self.batchsizeInput.initializer, feed_dict={ self.initializers['batchsizeInput']: feedinput[self.batchsizeInput]}) self.sess.run(self.learningrate.initializer, feed_dict={ self.initializers['learningrate']: feedinput[self.learningrate]}) self.sess.run(self.totalNumAtoms.initializer, feed_dict={ self.initializers['totalNumAtoms']: feedinput[self.totalNumAtoms]}) self.sess.run(self.nAtoms_forces.initializer, feed_dict={ self.initializers['nAtoms_forces']: feedinput[self.nAtoms_forces]}) if self.forces_in in feedinput: self.sess.run(self.forces_in.initializer, feed_dict={ self.initializers['forces_in']: feedinput[self.forces_in]}) self.sess.run(self.energycoefficient.initializer, feed_dict={ self.initializers['energycoefficient']: feedinput[self.energycoefficient]}) self.sess.run(self.forcecoefficient.initializer, feed_dict={ self.initializers['forcecoefficient']: feedinput[self.forcecoefficient]}) self.sess.run(self.energyProdScale.initializer, feed_dict={ self.initializers['energyProdScale']: feedinput[self.energyProdScale]}) # feeedinput={} def get_energy_list(self, hashs, fingerprintDB, fingerprintDerDB=None, keep_prob=1., input_keep_prob=1., forces=False, nsamples=1): """Methods to get the energy and forces for a set of configurations.""" # Make images a list in case we've been passed a single hash to # calculate. if not(isinstance(hashs, list)): hashs = [hashs] # Reformat the image and fingerprint data into something we can pass # into tensorflow. (atomArraysAll, nAtomsDict, atomsIndsReverse, natoms, dgdx, dgdx_Eindices, dgdx_Xindices) = \ generateTensorFlowArrays(fingerprintDB, self.elements, hashs, fingerprintDerDB) energies = np.zeros(len(hashs)) forcelist = np.zeros(len(hashs)) curinds = range(len(hashs)) (atomArraysFinal, dgdx_batch, dgdx_Eindices_batch, dgdx_Xindices_batch, atomInds) = generateBatch(curinds, self.elements, atomArraysAll, nAtomsDict, atomsIndsReverse, dgdx, dgdx_Eindices, dgdx_Xindices) feedinput = self.generateFeedInput(curinds, energies, atomArraysAll, dgdx, dgdx_Eindices, dgdx_Xindices, nAtomsDict, atomsIndsReverse, len(hashs), 1., 1., 1., natoms, forcesExp=forcelist, energycoefficient=1., forcecoefficient=int(forces), training=False) if self.weights is not None: self.setWeightsScalings(feedinput, self.weights, self.scalings) if nsamples == 1: energies = \ np.array(self.sess.run(self.energy, feed_dict=feedinput)) + \ self.parameters['energyMeanScale'] # Add in the per-atom base energy. natomsArray = np.zeros((len(hashs), len(self.elements))) for i in range(len(hashs)): for j in range(len(self.elements)): natomsArray[i][j] = nAtomsDict[self.elements[j]][i] if forces: force = self.sess.run(self.forces, feed_dict=feedinput) force = reorganizeForces(force, natoms) else: force = [] else: energysave = [] forcesave = [] # Add in the per-atom base energy. natomsArray = np.zeros((len(hashs), len(self.elements))) for i in range(len(hashs)): for j in range(len(self.elements)): natomsArray[i][j] = nAtomsDict[self.elements[j]][i] for samplenum in range(nsamples): energies = \ np.array(self.sess.run(self.energy, feed_dict=feedinput)) + \ self.parameters['energyMeanScale'] energysave.append(map(lambda x: x[0], energies)) if forces: force = self.sess.run(self.forces, feed_dict=feedinput) forcesave.append(reorganizeForces(force, natoms)) energies = np.array(energysave) force = np.array(forcesave) return energies, force def calculate_energy(self, fingerprint): """Get the energy by feeding in a list to the get_list version (which is more efficient for anything greater than 1 image).""" key = '1' energies, forces = self.get_energy_list([key], {key: fingerprint}) return energies[0] def getVariance(self, fingerprint, nSamples=10, l=1.): key = '1' # energies=[] # for i in range(nSamples): # energies.append(self.get_energy_list([key], {key: # fingerprint},keep_prob=self.keep_prob)[0]) energies, force = \ self.get_energy_list([key], {key: fingerprint}, keep_prob=self.keep_prob, nsamples=nSamples) if (('regularization_strength' in self.parameters) and (self.parameters['regularization_strength'] is not None)): tau = l**2. * self.keep_prob / \ (2 * self.parameters['numTrainingImages'] * self.parameters['regularization_strength']) var = np.var(energies) + tau**-1. # forcevar=np.var(forces,) else: tau = 1 var = np.var(energies) return var def calculate_forces(self, fingerprint, derfingerprint): # calculate_forces function still needs to be implemented. Can't do # this without the fingerprint derivates working properly though key = '1' energies, forces = \ self.get_energy_list([key], {key: fingerprint}, fingerprintDerDB={key: derfingerprint}, forces=True) return forces[0][0:len(fingerprint)] def tostring(self): """Dummy tostring to make things work.""" params = {} params['hiddenlayers'] = self.hiddenlayers params['keep_prob'] = self.keep_prob params['input_keep_prob'] = self.input_keep_prob params['elementFingerprintLengths'] = self.elementFingerprintLengths params['batchsize'] = self.batchsize params['maxTrainingEpochs'] = self.maxTrainingEpochs params['importname'] = self.importname params['initialTrainingRate'] = self.initialTrainingRate params['activation'] = self.activationName params['saveVariableName'] = self.saveVariableName params['parameters'] = self.parameters params['miniBatch'] = self.miniBatch params['optimizationMethod'] = self.optimizationMethod # Create a string format of the tensorflow variables. self.saver.save(self.sess, 'tfAmpNN-checkpoint') with open('tfAmpNN-checkpoint') as fhandle: params['tfVars'] = fhandle.read() return str(params) def model(x, segmentinds, keep_prob, input_keep_prob, batchsize, neuronList, activationType, fplength, mask, name, dgdx, dgdx_Xindices, dgdx_Eindices, element, unit_type, totalNumAtoms): """Generates a multilayer neural network with variable number of neurons, so that we have a template for each atom's NN.""" namefun = lambda x: '%s_%s_' % (name, element) + x nNeurons = neuronList[0] # Pass the input tensors through the first soft-plus layer W_fc = weight_variable( [fplength, nNeurons], name=namefun('Wfc0'), unit_type=unit_type) b_fc = bias_variable([nNeurons], name=namefun('bfc0'), unit_type=unit_type) input_dropout = tf.nn.dropout(x, input_keep_prob) # h_fc = activationType(tf.matmul(x, W_fc) + b_fc) h_fc = tf.nn.dropout( activationType(tf.matmul(input_dropout, W_fc) + b_fc), keep_prob) # l2_regularization=\ # tf.reduce_sum(tf.square(W_fc))+tf.reduce_sum(tf.square(b_fc)) l2_regularization = tf.reduce_sum(tf.square(W_fc)) if len(neuronList) > 1: for i in range(1, len(neuronList)): nNeurons = neuronList[i] nNeuronsOld = neuronList[i - 1] W_fc = weight_variable([nNeuronsOld, nNeurons], name=namefun('Wfc%d' % i), unit_type=unit_type) b_fc = bias_variable([nNeurons], name=namefun('bfc%d' % i), unit_type=unit_type) h_fc = tf.nn.dropout(activationType( tf.matmul(h_fc, W_fc) + b_fc), keep_prob) l2_regularization += tf.reduce_sum( tf.square(W_fc)) + tf.reduce_sum(tf.square(b_fc)) W_fc_out = weight_variable( [neuronList[-1], 1], name=namefun('Wfcout'), unit_type=unit_type) b_fc_out = bias_variable([1], name=namefun('bfcout'), unit_type=unit_type) y_out = tf.matmul(h_fc, W_fc_out) + b_fc_out l2_regularization += tf.reduce_sum( tf.square(W_fc_out)) + tf.reduce_sum(tf.square(b_fc_out)) # l2_regularization+=tf.reduce_sum(tf.square(W_fc_out))) # Sum the predicted energy for each molecule reducedSum = tf.unsorted_segment_sum(y_out, segmentinds, batchsize) dEjdgj = tf.gradients(y_out, x)[0] # expand for 3 components (x,y,z) # dEjdgj1 = tf.expand_dims(dEjdgj, 2) # dEjdgjtile = tf.tile(dEjdgj1, [1,1,3]) # Gather rows necessary based on the given partial derivatives (dg/dx) dEdg_arranged = tf.gather(dEjdgj, dgdx_Eindices) dEdg_arranged_expand = tf.expand_dims(dEdg_arranged, 2) dEdg_arranged_tile = tf.tile(dEdg_arranged_expand, [1, 1, 3]) # multiply through with the dg/dx tensor, and sum along the components of g # to get a tensor of dE/dx (one row per atom considered, second dim =3) dEdx = tf.reduce_sum(tf.mul(dEdg_arranged_tile, dgdx), 1) # this should be a tensor of size (total atoms in training set)x3, # representing the contribution of each atom to the total energy via # interactions with elements of the current atom type dEdx_arranged = tf.unsorted_segment_sum(dEdx, dgdx_Xindices, totalNumAtoms) return tf.mul(reducedSum, mask), dEdx_arranged, l2_regularization # dEg # dEjdgj1 = tf.expand_dims(dEjdgj, 1) # dEjdgj2 = tf.expand_dims(dEjdgj1, 1) # dEjdgjtile = tf.tile(dEjdgj2, tilederiv) # dEdxik = tf.mul(dxdxik, dEjdgjtile) # dEdxikReduce = tf.reduce_sum(dEdxik, 3) # dEdxik_reduced = tf.unsorted_segment_sum( # dEdxikReduce, segmentinds, batchsize) # return tf.mul(reducedSum, mask), dEdxik_reduced,l2_regularization def weight_variable(shape, name, unit_type, stddev=0.1): """Helper functions taken from the MNIST tutorial to generate weight and bias variables with random initial weights.""" initial = tf.truncated_normal(shape, stddev=stddev, dtype=unit_type) return tf.Variable(initial, name=name) def bias_variable(shape, name, unit_type, a=0.1): """Helper functions taken from the MNIST tutorial to generate weight and bias variables with random initial weights.""" initial = tf.truncated_normal(stddev=a, shape=shape, dtype=unit_type) return tf.Variable(initial, name=name) def generateBatch(curinds, elements, atomArraysAll, nAtomsDict, atomsIndsReverse, dgdx, dgdx_Eindices, dgdx_Xindices,): """This method generates batches from a large dataset using a set of selected indices curinds.""" # inputs: atomArraysFinal = {} for element in elements: validKeys = np.in1d(atomsIndsReverse[element], curinds) if len(validKeys) > 0: atomArraysFinal[element] = atomArraysAll[element][validKeys] else: atomArraysFinal[element] = [] dgdx_out = {} dgdx_Eindices_out = {} dgdx_Xindices_out = {} for element in elements: if len(dgdx[element]) > 0: dgdx_out[element] = [] dgdx_Eindices_out[element] = [] dgdx_Xindices_out[element] = [] cursumE = 0 cursumX = 0 for curind in curinds: natomsElement = nAtomsDict[element][curind] natomsTotal = np.sum( map(lambda x: nAtomsDict[x][curind], elements)) if len(dgdx_Eindices[element][curind]) > 0: dgdx_out[element].append(dgdx[element][curind]) dgdx_Eindices_out[element].append( dgdx_Eindices[element][curind] + cursumE) dgdx_Xindices_out[element].append( dgdx_Xindices[element][curind] + cursumX) cursumE += natomsElement cursumX += natomsTotal if len(dgdx_out[element]) > 0: dgdx_out[element] = np.concatenate(dgdx_out[element], axis=0) dgdx_Eindices_out[element] = np.concatenate( dgdx_Eindices_out[element], axis=0) dgdx_Xindices_out[element] = np.concatenate( dgdx_Xindices_out[element], axis=0) else: dgdx_out[element] = np.array([[]]) dgdx_Eindices_out[element] = np.array([]) dgdx_Xindices_out[element] = np.array([]) else: dgdx_out[element] = np.array([[[]]]) dgdx_Eindices_out[element] = np.array([]) dgdx_Xindices_out[element] = np.array([]) atomInds = {} for element in elements: validKeys = np.in1d(atomsIndsReverse[element], curinds) if len(validKeys) > 0: atomIndsTemp = np.sum(atomsIndsReverse[element][validKeys], 1) atomInds[element] = atomIndsTemp * 0. for i in range(len(curinds)): atomInds[element][atomIndsTemp == curinds[i]] = i else: atomInds[element] = [] return (atomArraysFinal, dgdx_out, dgdx_Eindices_out, dgdx_Xindices_out, atomInds) def generateTensorFlowArrays(fingerprintDB, elements, keylist, fingerprintDerDB=None): """ This function generates the inputs to the tensorflow graph for the selected images. The essential problem is that each neural network is associated with a specific element type. Thus, atoms in each ASE image need to be sent to different networks. Inputs: fingerprintDB: a database of fingerprints, as taken from the descriptor elements: a list of element types (e.g. 'C','O', etc) keylist: a list of hashs into the fingerprintDB that we want to create inputs for fingerprintDerDB: a database of fingerprint derivatives, as taken from the descriptor maxAtomsForces: the maximum length of the atoms Outputs: atomArraysAll: a dictionary of fingerprint inputs to each element's neural network nAtomsDict: a dictionary for each element with lists of the number of atoms of each type in each image atomsIndsReverse: a dictionary that contains the index of each atom into the original keylist nAtoms: the number of atoms in each image atomArraysAllDerivs: dictionary of fingerprint derivates for each element's neural network """ nAtomsDict = {} for element in elements: nAtomsDict[element] = np.zeros(len(keylist)) for j in range(len(keylist)): fp = fingerprintDB[keylist[j]] atomSymbols, fpdata = zip(*fp) for i in range(len(fp)): nAtomsDict[atomSymbols[i]][j] += 1 atomsPositions = {} for element in elements: atomsPositions[element] = np.cumsum( nAtomsDict[element]) - nAtomsDict[element] atomsIndsReverse = {} for element in elements: atomsIndsReverse[element] = [] for i in range(len(keylist)): if nAtomsDict[element][i] > 0: atomsIndsReverse[element].append( np.ones((nAtomsDict[element][i].astype(np.int64), 1)) * i) if len(atomsIndsReverse[element]) > 0: atomsIndsReverse[element] = np.concatenate( atomsIndsReverse[element]) atomArraysAll = {} for element in elements: atomArraysAll[element] = [] natoms = np.zeros((len(keylist), 1)) for j in range(len(keylist)): fp = fingerprintDB[keylist[j]] atomSymbols, fpdata = zip(*fp) atomdata = zip(atomSymbols, range(len(atomSymbols))) for element in elements: atomArraysTemp = [] curatoms = [atom for atom in atomdata if atom[0] == element] for i in range(len(curatoms)): atomArraysTemp.append(fp[curatoms[i][1]][1]) if len(atomArraysTemp) > 0: atomArraysAll[element].append(atomArraysTemp) natoms[j] = len(atomSymbols) natomsposition = np.cumsum(natoms) - natoms[0] for element in elements: if len(atomArraysAll[element]) > 0: atomArraysAll[element] = np.concatenate(atomArraysAll[element]) else: atomArraysAll[element] = [] # Set up the array for atom-based fingerprint derivatives. dgdx = {} dgdx_Eindices = {} dgdx_Xindices = {} for element in elements: dgdx[element] = [] # Nxlen(fp)x3 array dgdx_Eindices[element] = [] # Nx1 array of which dE/dg to pull dgdx_Xindices[element] = [] # Nx1 array representing which atom this force will represent if fingerprintDerDB is not None: for j in range(len(keylist)): fp = fingerprintDB[keylist[j]] fpDer = fingerprintDerDB[keylist[j]] atomSymbols, fpdata = zip(*fp) atomdata = zip(atomSymbols, range(len(atomSymbols))) for element in elements: curatoms = [atom for atom in atomdata if atom[0] == element] dgdx_temp = [] dgdx_Eindices_temp = [] dgdx_Xindices_temp = [] if len(curatoms) > 0: for i in range(len(curatoms)): for k in range(len(atomdata)): # check if fp derivative is present dictkeys = [(k, atomdata[k][0], curatoms[ i][1], curatoms[i][0], 0), (k, atomdata[k][0], curatoms[ i][1], curatoms[i][0], 1), (k, atomdata[k][0], curatoms[ i][1], curatoms[i][0], 2)] if ((dictkeys[0] in fpDer) or (dictkeys[1] in fpDer) or (dictkeys[2] in fpDer)): fptemp = [] for ix in range(3): dictkey = (k, atomdata[k][0], curatoms[ i][1], curatoms[i][0], ix) fptemp.append(fpDer[dictkey]) dgdx_temp.append(np.array(fptemp).transpose()) dgdx_Eindices_temp.append(i) dgdx_Xindices_temp.append(k) if len(dgdx_Eindices_temp) > 0: dgdx[element].append(np.array(dgdx_temp)) dgdx_Eindices[element].append(np.array(dgdx_Eindices_temp)) dgdx_Xindices[element].append(np.array(dgdx_Xindices_temp)) else: dgdx[element].append([]) dgdx_Eindices[element].append([]) dgdx_Xindices[element].append([]) return (atomArraysAll, nAtomsDict, atomsIndsReverse, natoms, dgdx, dgdx_Eindices, dgdx_Xindices) def reorganizeForces(forces, natoms): curoffset = 0 forcelist = [] for N in natoms: forcelist.append(forces[curoffset:curoffset + N[0].astype(np.int64)]) curoffset += N[0] return forcelist andrewpeterson-amp-4878fc892f2c/amp/regression/000077500000000000000000000000001332417112400214035ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/regression/__init__.py000066400000000000000000000102061332417112400235130ustar00rootroot00000000000000from ..utilities import ConvergenceOccurred class Regressor: """Class to manage the regression of a generic model. That is, for a given parameter set, calculates the cost function (the difference in predicted energies and actual energies across training images), then decides how to adjust the parameters to reduce this cost function. Global optimization conditioners (e.g., simulated annealing, etc.) can be built into this class. Parameters ---------- optimizer : str The optimizer to use. Several defaults are available including 'L-BFGS-B', 'BFGS', 'TNC', or 'NCG'. Alternatively, any function can be supplied which behaves like scipy.optimize.fmin_bfgs. optimizer_kwargs : dict Optional keywords for the corresponding optimizer. lossprime : boolean Decides whether or not the regressor needs to be fed in by gradient of the loss function as well as the loss function itself. """ def __init__(self, optimizer='BFGS', optimizer_kwargs=None, lossprime=True): """optimizer can be specified; it should behave like a scipy.optimize optimizer. That is, it should take as its first two arguments the function to be optimized and the initial guess of the optimal paramters. Additional keyword arguments can be fed through the optimizer_kwargs dictionary.""" user_kwargs = optimizer_kwargs optimizer_kwargs = {} if optimizer == 'BFGS': from scipy.optimize import fmin_bfgs as optimizer optimizer_kwargs = {'gtol': 1e-15, } elif optimizer == 'L-BFGS-B': from scipy.optimize import fmin_l_bfgs_b as optimizer optimizer_kwargs = {'factr': 1e+02, 'pgtol': 1e-08, 'maxfun': 1000000, 'maxiter': 1000000} import scipy from distutils.version import StrictVersion if StrictVersion(scipy.__version__) >= StrictVersion('0.17.0'): optimizer_kwargs['maxls'] = 2000 elif optimizer == 'TNC': from scipy.optimize import fmin_tnc as optimizer optimizer_kwargs = {'ftol': 0., 'xtol': 0., 'pgtol': 1e-08, 'maxfun': 1000000, } elif optimizer == 'NCG': from scipy.optimize import fmin_ncg as optimizer optimizer_kwargs = {'avextol': 1e-15, } if user_kwargs: optimizer_kwargs.update(user_kwargs) self.optimizer = optimizer self.optimizer_kwargs = optimizer_kwargs self.lossprime = lossprime def regress(self, model, log): """Performs the regression. Calls model.get_loss, which should return the current value of the loss function until convergence has been reached, at which point it should raise a amp.utilities.ConvergenceException. Parameters ---------- model : object Class representing the regression model. log : str Name of script to log progress. """ log('Starting parameter optimization.', tic='opt') log(' Optimizer: %s' % self.optimizer) log(' Optimizer kwargs: %s' % self.optimizer_kwargs) x0 = model.vector.copy() try: if self.lossprime: self.optimizer(model.get_loss, x0, model.get_lossprime, **self.optimizer_kwargs) else: self.optimizer(model.get_loss, x0, **self.optimizer_kwargs) except ConvergenceOccurred: log('...optimization successful.', toc='opt') return True else: log('...optimization unsuccessful.', toc='opt') if self.lossprime: max_lossprime = \ max(abs(max(model.lossfunction.dloss_dparameters)), abs(min(model.lossfunction.dloss_dparameters))) log('...maximum absolute value of loss prime: %.3e' % max_lossprime) return False andrewpeterson-amp-4878fc892f2c/amp/stats/000077500000000000000000000000001332417112400203615ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/stats/__init__.py000066400000000000000000000000001332417112400224600ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/amp/stats/bootstrap.py000066400000000000000000000356711332417112400227640ustar00rootroot00000000000000import os import sys import shutil import numpy as np from string import Template import time import json from StringIO import StringIO from scipy.stats.mstats import mquantiles import tarfile import tempfile import ase.io from ..utilities import hash_images, Logger from .. import Amp calc_text = """ from amp import Amp from amp.descriptor.gaussian import Gaussian from amp.model.neuralnetwork import NeuralNetwork calc = Amp(descriptor=Gaussian(), model=NeuralNetwork(), dblabel='../amp-db') """ train_line = "calc.train(images=hashed_images)" script = """#!/usr/bin/env python ${headerlines} from amp.utilities import TrainingConvergenceError, hash_images from ase.parallel import paropen from bunquant.bootstrap import hash_with_duplicates import os ${calc_text} ensemble_index = int(os.path.split(os.getcwd())[-1]) trainfile = '../training-images/%i.traj' % ensemble_index hashed_images = hash_with_duplicates(trainfile) converged = True try: ${train_line} except TrainingConvergenceError: converged = False f = paropen('converged', 'w') f.write(str(converged)) f.close() """ class BootStrap: """A bootstrap ensemble calculator which serves as a wrapper around and Amp calculator. Initiate with an amp.utilities.Logger instance as log. If an existing trained bootstrap calculator is available, it can be loaded by providing its filename to the load keyword. Note that the 'train' method is meant to be a job-submission and -management script; e.g., it will typically be run at the command line to both submit jobs and monitor their convergence. """ def __init__(self, load=None, log=None): if log is None: log = Logger(sys.stdout) self.log = log if load is None: return with open(load) as f: calctexts = json.load(f) self.ensemble = [] for calctext in calctexts: f = StringIO(calctext) calc = Amp.load(file=f) calc.log = Logger(None) self.ensemble.append(calc) log('Loaded ensemble of %i calculators.' % len(self.ensemble)) def train(self, images, n=50, calc_text=calc_text, headerlines='', start_command='python run.py', sleep=0.1, train_line=train_line, label='bootstrap'): """Trains a bootstrap ensemble of calculators. This is set up to enable the submision of each as a job through the local queuing system, but can also run in serial. On first call to this method, jobs are created/submitted. On subsequent calls, jobs are analyzed for convergence. If all are converged, an ensemble is created and the training directory is archived. Creates a lot of individual runs in directories n: int size of ensemble (number of calculators to train) calc_text: text text that is used to initiate the Amp calculator. see the example in this module in calc_text; must produce a 'calc' object headerlines: text lines in the top of the python script that will be submitted this would typically contain comment lines for the batching system, such as '#SBATCH -n=3\n #SBATCH -cores=8\n...' start_command: text command to start the job in the current queuing system, such as 'sbatch run.py' ('run.py' is the scriptname here) for serial operation use 'python run.py' sleep : float time (s) to sleep between job submissions train_line: text line to use to train each amp instance; usually the default is fine but user may want to use this to insert additional keywords such as train_forces=False label: string label to give final trained calculator """ log = self.log trainingpath = '-'.join((label, 'training')) if os.path.exists(trainingpath): log('Path exists. Checking for which jobs are finished.') n_unfinished = 0 n_converged = 0 n_unconverged = 0 pwd = os.getcwd() os.chdir(trainingpath) fulltrainingpath = os.getcwd() for index in range(n): os.chdir('%i' % index) if not os.path.exists('converged'): log('%i: Still running? No converged file.' % index) os.chdir(fulltrainingpath) n_unfinished += 1 continue with open('converged') as f: converged = f.read() if converged == 'True': log('%i: Converged.' % index) n_converged += 1 else: log('%i: Not converged. Cleaning up directory to ' ' restart job.' % index) n_unconverged += 1 for _ in os.listdir(os.getcwd()): if _ != 'run.py': if os.path.isdir(_): shutil.rmtree(_) else: os.remove(_) os.system(start_command) time.sleep(sleep) log('%i: Restarted.') os.chdir(fulltrainingpath) log('') log('Stats:') log('%10i converged' % n_converged) log('%10i did not converge, restarted' % n_unconverged) log('%10i apparently still running' % n_unfinished) log('=' * 10) log('%10i total' % n) log('\n') if n_converged < n: log('Not all runs converged; not creating bundled amp ' 'calculator.') return log('Creating bundled amp calculator.') ensemble = [] for index in range(n): os.chdir('%i' % index) with open('amp.amp') as f: text = f.read() ensemble.append(text) os.chdir(fulltrainingpath) os.chdir(pwd) with open('%s.ensemble' % label, 'w') as f: json.dump(ensemble, f) log('Saved in json format as "%s.ensemble".' % label) log('Converting training directory into tar archive...') archive_directory(trainingpath) log('...converted.') return log('Training set: ' + str(images)) images = hash_images(images) log('%i images in training set after hashing.' % len(images)) image_keys = images.keys() originalpath = os.getcwd() trajpath = os.path.join(trainingpath, 'training-images') os.mkdir(trainingpath) os.mkdir(trajpath) log('Creating bootstrapped training images in %s.' % trajpath) for index in range(n): log(' Choosing images for %i.' % index) chosen = bootstrap(image_keys) log(' Writing trajectory for %i.' % index) traj = ase.io.Trajectory( os.path.join(trajpath, '%i.traj' % index), 'w') for key in chosen: traj.write(images[key]) log('Creating and submitting jobs.') os.chdir(trainingpath) template = Template(script) pwd = os.getcwd() for index in range(n): os.mkdir('%i' % index) os.chdir('%i' % index) with open('run.py', 'w') as f: f.write(template.substitute({'headerlines': headerlines, 'calc_text': calc_text, 'train_line': train_line})) os.system(start_command) time.sleep(sleep) os.chdir(pwd) os.chdir(originalpath) def get_potential_energy(self, atoms, output=(.5,)): """Returns the potential energy from the ensemble for the atoms object. By default only returns the median prediction (50th percentile) of the ensemble, such that it works like a normal ASE calculator. To get uncertainty information, use the output keyword with the following codes: : (where is a float) return the q quantile of the ensemble (where the quantile is a decimal, as in 0.5 for 50th percentile) e: return the whole ensemble prediction as a list Join the arguments with commas. For example, to return the median prediction plus a centered spread covering 90% of the ensemble prediction, use output=[.5, .05, .95]. If the ensemble is requested, it must be the last argument, e.g., output=[.5, .025, .97.5, 'e']. Note a list is typically returned, but if only one attribute is requested it returns it as a float, so that it's ASE-like. """ energies = [calc.get_potential_energy(atoms) for calc in self.ensemble] if output[-1] == 'e': quantiles = output[:-1] return_ensemble = True else: quantiles = output return_ensemble = False for quantile in quantiles: if (quantile > 1.0) or (quantile < 0.0): raise RuntimeError('Quantiles must be between 0 and 1.') result = mquantiles(energies, prob=quantiles) result = list(result) if return_ensemble: result.append(energies) if len(result) == 1: result == result[0] return result def get_forces(self, atoms, output=(.5,)): """Returns the atomic forces from the ensemble for the atoms object. By default only returns the median prediction (50th percentile) of the ensemble, such that it works like a normal ASE calculator. To get uncertainty information, use the output keyword with the following codes: : (where is a float) return the q quantile of the ensemble (where the quantile is a decimal, as in 0.5 for 50th percentile) e: return the whole ensemble prediction as a list Join the arguments with commas. For example, to return the median prediction plus a centered spread covering 90% of the ensemble prediction, use output=[.5, .05, .95]. If the ensemble is requested, it must be the last argument, e.g., output=[.5, .025, .97.5, 'e']. Note a list is typically returned, but if only one attribute is requested it returns it as a float, so that it's ASE-like. """ forces = [calc.get_forces(atoms) for calc in self.ensemble] forces = np.array(forces) if output[-1] == 'e': quantiles = output[:-1] return_ensemble = True else: quantiles = output return_ensemble = False for quantile in quantiles: if (quantile > 1.0) or (quantile < 0.0): raise RuntimeError('Quantiles must be between 0 and 1.') # FIXME/ap: Had to switch to np.percentile from scipy mquantiles. # Because mquantiles doesn't support higher dimensions. # Should probably switch to percentiles throughout the code as # it's easier to read. percentiles = np.array(quantiles) * 100. result = np.percentile(forces, percentiles, axis=0) result = list(result) if return_ensemble: result.append(forces) if len(result) == 1: result == result[0] return result def get_atomic_energies(self, atoms, output=(.5,)): """ Returns the energy per atom from ensemble. The output parameter works as get_potential_energy.""" if output[-1] == 'e': quantiles = output[:-1] return_ensemble = True else: quantiles = output return_ensemble = False for quantile in quantiles: if (quantile > 1.0) or (quantile < 0.0): raise RuntimeError('Percentiles must be between 0 and 1.') self.get_potential_energy(atoms) # Assure calculation is fresh. atomic_energies = np.array([calc.model.atomic_energies for calc in self.ensemble]) result = mquantiles(atomic_energies, prob=quantiles, axis=0) result = list(result) if return_ensemble: result.append(atomic_energies) if len(result) == 1: result == result[0] return result def bootstrap(vector, size=None, return_missing=False): """Returns a randomly chosen, with replacement, version of the data set. If size is None returns a vector of same length. To pull from sample from multiple vectors, zip and unzip them like: >>> xsbs, ysbs = zip(*bootstrap(zip(xs, ys))) If return_missing == True, also finds and returns the missing elements not sampled from the vector as a second output. """ size = len(vector) if size is None else size ids = np.random.choice(len(vector), size=size, replace=True) chosen = [vector[_] for _ in ids] if return_missing is False: return chosen unchosen = set(range(len(vector))).difference(set(ids)) unchosen = [vector[_] for _ in unchosen] return chosen, unchosen def hash_with_duplicates(images): """Creates new hash id's for duplicate images; new dictionary contains a redundant copy of each atoms object, so that the lossfunctions can be used as-is. Note will typically waste ~30% of the computational cost; it would be more efficient to update the calls inside the loss functions.""" if not hasattr(images, 'keys'): images = hash_images(images) duplicates = images.metadata['duplicates'] dict_images = dict(images) for oldhash, repititions in duplicates.iteritems(): for repitition in range(repititions - 1): newhash = '-'.join([oldhash, '%i' % (repitition + 1)]) assert newhash not in dict_images dict_images[newhash] = images[oldhash] return dict_images def archive_directory(source_dir): """Turns into a .tar.gz file and removes the original directory.""" outputname = source_dir + '.tar.gz' if os.path.exists(outputname): raise RuntimeError('%s exists.' % outputname) with tarfile.open(outputname, 'w:gz') as tar: tar.add(source_dir) shutil.rmtree(source_dir) class TrainingArchive: """Helper to get training trajectories and Amp calc instances from the training tar ball. Initialize with archive name. The get commands use the path the file would have had if the archive were extracted.""" def __init__(self, name): self.tf = tarfile.open(name) def get_trajectory(self, path): # Doesn't work with extractfile because of numpy bug. tempdir = tempfile.mkdtemp() self.tf.extract(member=path, path=tempdir) return ase.io.Trajectory(os.path.join(tempdir, path)) def get_amp_calc(self, path): return Amp.load(self.tf.extractfile(path)) andrewpeterson-amp-4878fc892f2c/amp/utilities.py000066400000000000000000001223251332417112400216150ustar00rootroot00000000000000#!/usr/bin/env python import numpy as np import hashlib import time import os import sys import copy import math import random import signal import tarfile import traceback from datetime import datetime from getpass import getuser from ase import io as aseio from ase.db import connect from ase.calculators.calculator import PropertyNotImplementedError try: import cPickle as pickle # Python2 except ImportError: import pickle # Python3 # Parallel processing ######################################################## def assign_cores(cores, log=None): """Tries to guess cores from environment. If fed a log object, will write its progress. """ log = Logger(None) if log is None else log def fail(q, traceback_text=None): msg = ('Auto core detection is either not set up or not working for' ' your version of %s. You are invited to submit a patch to ' 'return a dictionary of the form {nodename: ncores} for this' ' batching system. The environment contents were dumped to ' 'the log file, as well as any traceback that caused the ' 'error.') log(msg % q) log('Environment dump:') for key, value in os.environ.items(): log('%s: %s' % (key, value)) if traceback_text: log('\n' + '='*70 + '\nTraceback of last error encountered:') log(traceback_text) raise NotImplementedError(msg % q) def success(q, cores, log): log('Parallel configuration determined from environment for %s:' % q) for key, value in cores.items(): log(' %s: %i' % (key, value)) if cores is not None: q = '' if cores == 1: log('Serial operation on one core specified.') return cores else: try: cores = int(cores) except TypeError: cores = cores success(q, cores, log) return cores else: cores = {'localhost': cores} success(q, cores, log) return cores if 'SLURM_NODELIST' in os.environ.keys(): q = 'SLURM' try: nnodes = int(os.environ['SLURM_NNODES']) taskspernode = int(os.environ['SLURM_NTASKS_PER_NODE']) if nnodes == 1: cores = {'localhost': taskspernode} else: nodes = os.environ['SLURM_NODELIST'] if '[' in nodes: # Formatted funny like 'node[572,578]'. prename, numbers = nodes.split('[') numbers = numbers[:-1].split(',') nodes = [prename + _ for _ in numbers] else: nodes = nodes.split(',') cores = {node: taskspernode for node in nodes} except: # Get the traceback to log it. fail(q, traceback_text=traceback.format_exc()) elif 'PBS_NODEFILE' in os.environ.keys(): fail(q='PBS') elif 'LOADL_PROCESSOR_LIST' in os.environ.keys(): fail(q='LOADL') elif 'PE_HOSTFILE' in os.environ.keys(): q = 'SGE' try: hostfile = os.getenv('PE_HOSTFILE') cores = {} with open(hostfile) as f: for i, istr in enumerate(f): hostname, nc = istr.split()[0:2] nc = int(nc) cores[hostname] = nc except: # Get the traceback to log it. fail(q, traceback_text=traceback.format_exc()) else: import multiprocessing ncores = multiprocessing.cpu_count() cores = {'localhost': ncores} log('No queuing system detected; single machine assumed.') q = '' success(q, cores, log) return cores class MessageDictionary: """Standard container for all messages (typically requests, via zmq.context.socket.send_pyobj) sent from the workers to the master. This returns a simple dictionary. This is roughly email format. Initialize with process id (e.g., 'from'). Call with subject and data (body). """ def __init__(self, process_id): self._process_id = process_id def __call__(self, subject, data=None): d = {'id': self._process_id, 'subject': subject, 'data': data} return d def make_sublists(masterlist, n): """Randomly divides the masterlist into n sublists of roughly equal size. The intended use is to divide a keylist and assign keys to each task in parallel processing. This also destroys the masterlist (to save some memory). """ masterlist = list(masterlist) np.random.shuffle(masterlist) N = len(masterlist) sublist_lengths = [ N // n if _ >= (N % n) else N // n + 1 for _ in range(n)] sublists = [] for sublist_length in sublist_lengths: sublists.append([masterlist.pop() for _ in range(sublist_length)]) return sublists def setup_parallel(parallel, workercommand, log): """Starts the worker processes and the master to control them. This makes an SSH connection to each node (including the one the master process runs on), then creates the specified number of processes on each node through its SSH connection. Then sets up ZMQ for efficienty communication between the worker processes and the master process. Uses the parallel dictionary as defined in amp.Amp. log is an Amp logger. module is the name of the module to be called, which is usually given by self.calc.__module, etc. workercommand is stub of the command used to start the servers, typically like "python -m amp.descriptor.gaussian". Appended to this will be " &" where is the unique ID assigned to each process and is the address of the server, like 'node321:34292'. Returns ------- server : (a ZMQ socket) The ssh connections (pxssh instances; if these objects are destroyed pxssh will close the sessions) the pid_count, which is the total number of workers started. Each worker can be communicated directly through its PID, an integer between 0 and pid_count """ import zmq from socket import gethostname log(' Parallel processing.') serverhostname = gethostname() # Establish server session. context = zmq.Context() server = context.socket(zmq.REP) port = server.bind_to_random_port('tcp://*') serversocket = '%s:%s' % (serverhostname, port) log(' Established server at %s.' % serversocket) workercommand += ' %s ' + serversocket log(' Establishing worker sessions.') connections = [] pid_count = 0 for workerhostname, nprocesses in parallel['cores'].items(): pids = range(pid_count, pid_count + nprocesses) pid_count += nprocesses connections.append(start_workers(pids, workerhostname, workercommand, log, parallel['envcommand'])) return server, connections, pid_count def start_workers(process_ids, workerhostname, workercommand, log, envcommand): """A function to start a new SSH session and establish processes on that session. """ if workerhostname != 'localhost': workercommand += ' &' log(' Starting non-local connections.') pxssh = importer('pxssh') ssh = pxssh.pxssh() ssh.login(workerhostname, getuser()) if envcommand is not None: log('Environment command: %s' % envcommand) ssh.sendline(envcommand) ssh.readline() for process_id in process_ids: ssh.sendline(workercommand % process_id) ssh.expect('') ssh.expect('') log(' Session %i (%s): %s' % (process_id, workerhostname, ssh.before.strip())) return ssh import pexpect log(' Starting local connections.') children = [] for process_id in process_ids: child = pexpect.spawn(workercommand % process_id) child.expect('') child.expect('') log(' Session %i (%s): %s' % (process_id, workerhostname, child.before.strip())) children.append(child) return children # Data and logging ########################################################### class FileDatabase: """Using a database file, such as shelve or sqlitedict, that can handle multiple processes writing to the file is hard. Therefore, we take the stupid approach of having each database entry be a separate file. This class behaves essentially like shelve, but saves each dictionary entry as a plain pickle file within the directory, with the filename corresponding to the dictionary key (which must be a string). Like shelve, this also keeps an internal (memory dictionary) representation of the variables that have been accessed. Also includes an archive feature, where files are instead added to a file called 'archive.tar.gz' to save disk space. If an entry exists in both the loose and archive formats, the loose is taken to be the new (correct) value. """ def __init__(self, filename): """Open the filename at specified location. flag is ignored; this format is always capable of both reading and writing.""" if not filename.endswith(os.extsep + 'ampdb'): filename += os.extsep + 'ampdb' self.path = filename self.loosepath = os.path.join(self.path, 'loose') self.tarpath = os.path.join(self.path, 'archive.tar.gz') if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.loosepath) self._memdict = {} # Items already accessed; stored in memory. @classmethod def open(Cls, filename, flag=None): """Open present for compatibility with shelve. flag is ignored; this format is always capable of both reading and writing. """ return Cls(filename=filename) def close(self): """Only present for compatibility with shelve. """ return def keys(self): """Return list of keys, both of in-memory and out-of-memory items. """ keys = os.listdir(self.loosepath) if os.path.exists(self.tarpath): with tarfile.open(self.tarpath) as tf: keys = list(set(keys + tf.getnames())) return keys def values(self): """Return list of values, both of in-memory and out-of-memory items. This moves all out-of-memory items into memory. """ keys = self.keys() return [self[key] for key in keys] def __len__(self): return len(self.keys()) def __setitem__(self, key, value): self._memdict[key] = value path = os.path.join(self.loosepath, str(key)) if os.path.exists(path): with open(path, 'r') as f: if f.read() == pickle.dumps(value): return # Nothing to update. with open(path, 'wb') as f: pickle.dump(value, f) def __getitem__(self, key): if key in self._memdict: return self._memdict[key] keypath = os.path.join(self.loosepath, key) if os.path.exists(keypath): with open(keypath, 'rb') as f: return pickle.load(f) elif os.path.exists(self.tarpath): with tarfile.open(self.tarpath) as tf: return pickle.load(tf.extractfile(key)) else: raise KeyError(str(key)) def update(self, newitems): for key, value in newitems.items(): self.__setitem__(key, value) def archive(self): """Cleans up to save disk space and reduce huge number of files. That is, puts all files into an archive. Compresses all files in /loose and places them in /archive.tar.gz. If archive exists, appends/modifies. """ loosefiles = os.listdir(self.loosepath) print('Contains %i loose entries.' % len(loosefiles)) if len(loosefiles) == 0: print(' -> No action taken.') return if os.path.exists(self.tarpath): with tarfile.open(self.tarpath) as tf: names = [_ for _ in tf.getnames() if _ not in os.listdir(self.loosepath)] for name in names: tf.extract(member=name, path=self.loosepath) loosefiles = os.listdir(self.loosepath) print('Compressing %i entries.' % len(loosefiles)) with tarfile.open(self.tarpath, 'w:gz') as tf: for file in loosefiles: tf.add(name=os.path.join(self.loosepath, file), arcname=file) print('Cleaning up: removing %i files.' % len(loosefiles)) for file in loosefiles: os.remove(os.path.join(self.loosepath, file)) class Data: """Serves as a container (dictionary-like) for (key, value) pairs that also serves to calculate them. Works by default with python's shelve module, but something that is built to share the same commands as shelve will work fine; just specify this in dbinstance. Designed to hold things like neighborlists, which have a hash, value format. This will work like a dictionary in that items can be accessed with data[key], but other advanced dictionary functions should be accessed with through the .d attribute: >>> data = Data(...) >>> data.open() >>> keys = data.d.keys() >>> values = data.d.values() """ def __init__(self, filename, db=FileDatabase, calculator=None): self.calc = calculator self.db = db self.filename = filename self.d = None def calculate_items(self, images, parallel, log=None): """Calculates the data value with 'calculator' for the specified images. images is a dictionary, and the same keys will be used for the current database. """ if log is None: log = Logger(None) if self.d is not None: self.d.close() self.d = None log(' Data stored in file %s.' % self.filename) d = self.db.open(self.filename, 'r') calcs_needed = list(set(images.keys()).difference(d.keys())) dblength = len(d) d.close() log(' File exists with %i total images, %i of which are needed.' % (dblength, len(images) - len(calcs_needed))) log(' %i new calculations needed.' % len(calcs_needed)) if len(calcs_needed) == 0: return if parallel['cores'] == 1: d = self.db.open(self.filename, 'c') for key in calcs_needed: d[key] = self.calc.calculate(images[key], key) d.close() # Necessary to get out of write mode and unlock? log(' Calculated %i new images.' % len(calcs_needed)) else: python = sys.executable workercommand = '%s -m %s' % (python, self.calc.__module__) server, connections, n_pids = setup_parallel(parallel, workercommand, log) globals = self.calc.globals keyed = self.calc.keyed keys = make_sublists(calcs_needed, n_pids) results = {} # All incoming requests will be dictionaries with three keys. # d['id']: process id number, assigned when process created above. # d['subject']: what the message is asking for / telling you # d['data']: optional data passed from the worker. active = 0 # count of processes actively calculating log(' Parallel calculations starting...', tic='parallel') active = n_pids # currently active workers while True: message = server.recv_pyobj() if message['subject'] == '': server.send_pyobj(self.calc.parallel_command) elif message['subject'] == '': request = message['data'] # Variable name. if request == 'images': server.send_pyobj({k: images[k] for k in keys[int(message['id'])]}) elif request in keyed: server.send_pyobj({k: keyed[request][k] for k in keys[int(message['id'])]}) else: server.send_pyobj(globals[request]) elif message['subject'] == '': result = message['data'] server.send_string('meaningless reply') active -= 1 log(' Process %s returned %i results.' % (message['id'], len(result))) results.update(result) elif message['subject'] == '': server.send_string('meaningless reply') if active == 0: break log(' %i new results.' % len(results)) log(' ...parallel calculations finished.', toc='parallel') log(' Adding new results to database.') d = self.db.open(self.filename, 'c') d.update(results) d.close() # Necessary to get out of write mode and unlock? self.d = None def __getitem__(self, key): self.open() return self.d[key] def close(self): """Safely close the database. """ if self.d: self.d.close() self.d = None def open(self, mode='r'): """Open the database connection with mode specified. """ if self.d is None: self.d = self.db.open(self.filename, mode) def __del__(self): self.close() class Logger: """Logger that can also deliver timing information. Parameters ---------- file : str File object or path to the file to write to. Or set to None for a logger that does nothing. """ def __init__(self, file): if file is None: self.file = None return if isinstance(file, str): self.filename = file file = open(file, 'a') self.file = file self.tics = {} def tic(self, label=None): """Start a timer. Parameters ---------- label : str Label for managing multiple timers. """ if self.file is None: return if label: self.tics[label] = time.time() else: self._tic = time.time() def __call__(self, message, toc=None, tic=False): """Writes message to the log file. Parameters --------- message : str Message to be written. toc : bool or str If toc=True or toc=label, it will append timing information in minutes to the timer. tic : bool or str If tic=True or tic=label, will start the generic timer or a timer associated with label. Equivalent to self.tic(label). """ if self.file is None: return dt = '' if toc: if toc is True: tic = self._tic else: tic = self.tics[toc] dt = (time.time() - tic) / 60. dt = ' %.1f min.' % dt if self.file.closed: self.file = open(self.filename, 'a') self.file.write(message + dt + '\n') self.file.flush() if tic: if tic is True: self.tic() else: self.tic(label=tic) def make_filename(label, base_filename): """Creates a filename from the label and the base_filename which should be a string. Returns None if label is None; that is, it only saves output if a label is specified. Parameters ---------- label : str Prefix. base_filename : str Basic name of the file. """ if label is None: return None if not label: filename = base_filename else: filename = os.path.join(label + base_filename) return filename # Images and hashing ######################################################### def get_hash(atoms): """Creates a unique signature for a particular ASE atoms object. This is used to check whether an image has been seen before. This is just an md5 hash of a string representation of the atoms object. Parameters ---------- atoms : ASE dict ASE atoms object. Returns ------- Hash string key of 'atoms'. """ string = str(atoms.pbc) for number in atoms.cell.flatten(): string += '%.15f' % number string += str(atoms.get_atomic_numbers()) for number in atoms.get_positions().flatten(): string += '%.15f' % number md5 = hashlib.md5(string.encode('utf-8')) hash = md5.hexdigest() return hash def hash_images(images, log=None, ordered=False): """ Converts input images -- which may be a list, a trajectory file, or a database -- into a dictionary indexed by their hashes. Returns this dictionary. If ordered is True, returns an OrderedDict. When duplicate images are encountered (based on encountering an identical hash), a warning is written to the logfile. The number of duplicates of each image can be accessed by examinging dict_images.metadata['duplicates'], where dict_images is the returned dictionary. """ if log is None: log = Logger(None) if images is None: return elif hasattr(images, 'keys'): log(' %i unique images after hashing.' % len(images)) return images # Apparently already hashed. else: # Need to be hashed, and possibly read from file. if isinstance(images, str): log('Attempting to read images from file %s.' % images) extension = os.path.splitext(images)[1] from ase import io if extension == '.traj': images = io.Trajectory(images, 'r') elif extension == '.db': images = [row.toatoms() for row in connect(images, 'db').select(None)] # images converted to dictionary form; key is hash of image. log('Hashing images...', tic='hash') dict_images = MetaDict() dict_images.metadata['duplicates'] = {} dup = dict_images.metadata['duplicates'] if ordered is True: from collections import OrderedDict dict_images = OrderedDict() for image in images: hash = get_hash(image) if hash in dict_images.keys(): log('Warning: Duplicate image (based on identical hash).' ' Was this expected? Hash: %s' % hash) if hash in dup.keys(): dup[hash] += 1 else: dup[hash] = 2 dict_images[hash] = image log(' %i unique images after hashing.' % len(dict_images)) log('...hashing completed.', toc='hash') return dict_images def check_images(images, forces): """Checks that all images have energies, and optionally forces, calculated, so that they can be used for training. Raises a MissingDataError if any are missing.""" missing_energies, missing_forces = 0, 0 for index, image in enumerate(images.values()): try: image.get_potential_energy() except PropertyNotImplementedError: missing_energies += 1 if forces is True: try: image.get_forces() except PropertyNotImplementedError: missing_forces += 1 if missing_energies + missing_forces == 0: return msg = '' if missing_energies > 0: msg += 'Missing energy in {} image(s).'.format(missing_energies) if missing_forces > 0: msg += ' Missing forces in {} image(s).'.format(missing_forces) raise MissingDataError(msg) def randomize_images(images, fraction=0.8): """Randomly assigns 'fraction' of the images to a training set and (1 - 'fraction') to a test set. Returns two lists of ASE images. Parameters ---------- images : list or str List of ASE atoms objects in ASE format. This can also be the path to an ASE trajectory (.traj) or database (.db) file. fraction : float Portion of train_images to all images. Returns ------- train_images, test_images : list Lists of train and test images. """ file_opened = False if type(images) == str: extension = os.path.splitext(images)[1] if extension == '.traj': images = aseio.Trajectory(images, 'r') elif extension == '.db': images = aseio.read(images) file_opened = True trainingsize = int(fraction * len(images)) testsize = len(images) - trainingsize testindices = [] while len(testindices) < testsize: next = np.random.randint(len(images)) if next not in testindices: testindices.append(next) testindices.sort() trainindices = [index for index in range(len(images)) if index not in testindices] train_images = [images[index] for index in trainindices] test_images = [images[index] for index in testindices] if file_opened: images.close() return train_images, test_images # Custom exceptions ########################################################## class ConvergenceOccurred(Exception): """ Kludge to decide when scipy's optimizers are complete. """ pass class TrainingConvergenceError(Exception): """Error to be raised if training does not converge. """ pass class MissingDataError(Exception): """Error to be raised if any images are missing key data, like energy or forces.""" pass # Miscellaneous ############################################################## def string2dict(text): """Converts a string into a dictionary. Basically just calls `eval` on it, but supplies words like OrderedDict and matrix. """ try: dictionary = eval(text) except NameError: from collections import OrderedDict from numpy import array, matrix dictionary = eval(text) return dictionary def now(with_utc=False): """ Returns ------- String of current time. """ local = datetime.now().isoformat().split('.')[0] utc = datetime.utcnow().isoformat().split('.')[0] if with_utc: return '%s (%s UTC)' % (local, utc) else: return local logo = """ oo o o oooooo o o oo oo o o o o o o o o o o o o o o o o o o oooooooo o o o oooooo o o o o o o o o o o o o o o o """ def importer(name): """Handles strange import cases, like pxssh which might show up in eithr the package pexpect or pxssh. """ if name == 'pxssh': try: import pxssh except ImportError: try: from pexpect import pxssh except ImportError: raise ImportError('pxssh not found!') return pxssh elif name == 'NeighborList': try: from ase.neighborlist import NeighborList except ImportError: # We're on ASE 3.10 or older from ase.calculators.neighborlist import NeighborList return NeighborList # Amp Simulated Annealer ###################################################### class Annealer(object): """ Inspired by the simulated annealing implementation of Richard J. Wagner and Matthew T. Perry at https://github.com/perrygeo/simanneal. Performs simulated annealing by calling functions to calculate loss and make moves on a state. The temperature schedule for annealing may be provided manually or estimated automatically. Can be used by something like: >>> from amp import Amp >>> from amp.descriptor.gaussian import Gaussian >>> from amp.model.neuralnetwork import NeuralNetwork >>> calc = Amp(descriptor=Gaussian(), model=NeuralNetwork()) which will initialize tha calc object as usual, and then >>> from amp.utilities import Annealer >>> Annealer(calc=calc, images=images) which will perform simulated annealing global search in parameters space, and finally >>> calc.train(images=images) for gradient descent optimization. """ Tmax = 20.0 # Max (starting) temperature Tmin = 2.5 # Min (ending) temperature steps = 10000 # Number of iterations updates = steps / 200 # Number of updates (an update prints to log) copy_strategy = 'copy' user_exit = False save_state_on_exit = False def __init__(self, calc, images, Tmax=None, Tmin=None, steps=None, updates=None): if Tmax is not None: self.Tmax = Tmax if Tmin is not None: self.Tmin = Tmin if steps is not None: self.steps = steps if updates is not None: self.updates = updates self.calc = calc self.calc._log('\nAmp simulated annealer started. ' + now() + '\n') self.calc._log('Descriptor: %s' % self.calc.descriptor.__class__.__name__) self.calc._log('Model: %s' % self.calc.model.__class__.__name__) images = hash_images(images, log=self.calc._log) self.calc._log('\nDescriptor\n==========') # Derivatives of fingerprints need to be calculated if train_forces is # True. calculate_derivatives = True self.calc.descriptor.calculate_fingerprints( images=images, parallel=self.calc._parallel, log=self.calc._log, calculate_derivatives=calculate_derivatives) # Setting up calc.model.vector() self.calc.model.fit(trainingimages=images, descriptor=self.calc.descriptor, log=self.calc._log, parallel=self.calc._parallel, only_setup=True,) # Truning off ConvergenceOccured exception and log_losses initial_raise_ConvergenceOccurred = \ self.calc.model.lossfunction.raise_ConvergenceOccurred initial_log_losses = self.calc.model.lossfunction.log_losses self.calc.model.lossfunction.log_losses = False self.calc.model.lossfunction.raise_ConvergenceOccurred = False initial_state = self.calc.model.vector.copy() self.state = self.copy_state(initial_state) signal.signal(signal.SIGINT, self.set_user_exit) self.calc._log('\nAnnealing\n=========\n') bestState, bestLoss = self.anneal() # Taking the best state self.calc.model.vector = np.array(bestState) # Returning back the changed arguments self.calc.model.lossfunction.log_losses = initial_log_losses self.calc.model.lossfunction.raise_ConvergenceOccurred = \ initial_raise_ConvergenceOccurred # cleaning up sessions self.calc.model.lossfunction._step = 0 self.calc.model.lossfunction._cleanup() calc = self.calc @staticmethod def round_figures(x, n): """Returns x rounded to n significant figures.""" return round(x, int(n - math.ceil(math.log10(abs(x))))) @staticmethod def time_string(seconds): """Returns time in seconds as a string formatted HHHH:MM:SS.""" s = int(round(seconds)) # round to nearest second h, s = divmod(s, 3600) # get hours and remainder m, s = divmod(s, 60) # split remainder into minutes and seconds return '%4i:%02i:%02i' % (h, m, s) def save_state(self, fname=None): """Saves state """ if not fname: date = datetime.datetime.now().isoformat().split(".")[0] fname = date + "_loss_" + str(self.get_loss()) + ".state" print("Saving state to: %s" % fname) with open(fname, "w") as fh: pickle.dump(self.state, fh) def move(self, state): """Create a state change """ move_step = np.random.rand(len(state)) * 2. - 1. move_step *= 0.0005 for _ in range(len(state)): state[_] = state[_] * (1 + move_step[_]) return state def get_loss(self, state): """Calculate state's loss """ lossfxn = \ self.calc.model.lossfunction.get_loss(np.array(state), lossprime=False,)['loss'] return lossfxn def set_user_exit(self, signum, frame): """Raises the user_exit flag, further iterations are stopped """ self.user_exit = True def set_schedule(self, schedule): """Takes the output from `auto` and sets the attributes """ self.Tmax = schedule['tmax'] self.Tmin = schedule['tmin'] self.steps = int(schedule['steps']) def copy_state(self, state): """Returns an exact copy of the provided state Implemented according to self.copy_strategy, one of * deepcopy : use copy.deepcopy (slow but reliable) * slice: use list slices (faster but only works if state is list-like) * method: use the state's copy() method """ if self.copy_strategy == 'deepcopy': return copy.deepcopy(state) elif self.copy_strategy == 'slice': return state[:] elif self.copy_strategy == 'copy': return state.copy() def update(self, step, T, L, acceptance, improvement): """Prints the current temperature, loss, acceptance rate, improvement rate, elapsed time, and remaining time. The acceptance rate indicates the percentage of moves since the last update that were accepted by the Metropolis algorithm. It includes moves that decreased the loss, moves that left the loss unchanged, and moves that increased the loss yet were reached by thermal excitation. The improvement rate indicates the percentage of moves since the last update that strictly decreased the loss. At high temperatures it will include both moves that improved the overall state and moves that simply undid previously accepted moves that increased the loss by thermal excititation. At low temperatures it will tend toward zero as the moves that can decrease the loss are exhausted and moves that would increase the loss are no longer thermally accessible. """ elapsed = time.time() - self.start if step == 0: self.calc._log('\n') header = ' %5s %12s %12s %7s %7s %10s %10s' self.calc._log(header % ('Step', 'Temperature', 'Loss (SSD)', 'Accept', 'Improve', 'Elapsed', 'Remaining')) self.calc._log(header % ('=' * 5, '=' * 12, '=' * 12, '=' * 7, '=' * 7, '=' * 10, '=' * 10,)) self.calc._log( ' %5i %12.2e %12.4e %s ' % (step, T, L, self.time_string(elapsed))) else: remain = (self.steps - step) * (elapsed / step) self.calc._log(' %5i %12.2e %12.4e %7.2f%% %7.2f%% %s %s' % (step, T, L, 100.0 * acceptance, 100.0 * improvement, self.time_string(elapsed), self.time_string(remain))) def anneal(self): """Minimizes the loss of a system by simulated annealing. Parameters --------- state An initial arrangement of the system Returns ------- state, loss The best state and loss found. """ step = 0 self.start = time.time() # Precompute factor for exponential cooling from Tmax to Tmin if self.Tmin <= 0.0: raise Exception('Exponential cooling requires a minimum "\ "temperature greater than zero.') Tfactor = -math.log(self.Tmax / self.Tmin) # Note initial state T = self.Tmax L = self.get_loss(self.state) prevState = self.copy_state(self.state) prevLoss = L bestState = self.copy_state(self.state) bestLoss = L trials, accepts, improves = 0, 0, 0 if self.updates > 0: updateWavelength = self.steps / self.updates self.update(step, T, L, None, None) # Attempt moves to new states while step < (self.steps - 1) and not self.user_exit: step += 1 T = self.Tmax * math.exp(Tfactor * step / self.steps) self.state = self.move(self.state) L = self.get_loss(self.state) dL = L - prevLoss trials += 1 if dL > 0.0 and math.exp(-dL / T) < random.random(): # Restore previous state self.state = self.copy_state(prevState) L = prevLoss else: # Accept new state and compare to best state accepts += 1 if dL < 0.0: improves += 1 prevState = self.copy_state(self.state) prevLoss = L if L < bestLoss: bestState = self.copy_state(self.state) bestLoss = L if self.updates > 1: if step // updateWavelength > (step - 1) // updateWavelength: self.update( step, T, L, accepts / trials, improves / trials) trials, accepts, improves = 0, 0, 0 # line break after progress output print('') self.state = self.copy_state(bestState) if self.save_state_on_exit: self.save_state() # Return best state and loss return bestState, bestLoss def auto(self, minutes, steps=2000): """Minimizes the loss of a system by simulated annealing with automatic selection of the temperature schedule. Keyword arguments: state -- an initial arrangement of the system minutes -- time to spend annealing (after exploring temperatures) steps -- number of steps to spend on each stage of exploration Returns the best state and loss found. """ def run(T, steps): """Anneals a system at constant temperature and returns the state, loss, rate of acceptance, and rate of improvement. """ L = self.get_loss() prevState = self.copy_state(self.state) prevLoss = L accepts, improves = 0, 0 for step in range(steps): self.move() L = self.get_loss() dL = L - prevLoss if dL > 0.0 and math.exp(-dL / T) < random.random(): self.state = self.copy_state(prevState) L = prevLoss else: accepts += 1 if dL < 0.0: improves += 1 prevState = self.copy_state(self.state) prevLoss = L return L, float(accepts) / steps, float(improves) / steps step = 0 self.start = time.time() # Attempting automatic simulated anneal... # Find an initial guess for temperature T = 0.0 L = self.get_loss() self.update(step, T, L, None, None) while T == 0.0: step += 1 self.move() T = abs(self.get_loss() - L) # Search for Tmax - a temperature that gives 98% acceptance L, acceptance, improvement = run(T, steps) step += steps while acceptance > 0.98: T = self.round_figures(T / 1.5, 2) L, acceptance, improvement = run(T, steps) step += steps self.update(step, T, L, acceptance, improvement) while acceptance < 0.98: T = self.round_figures(T * 1.5, 2) L, acceptance, improvement = run(T, steps) step += steps self.update(step, T, L, acceptance, improvement) Tmax = T # Search for Tmin - a temperature that gives 0% improvement while improvement > 0.0: T = self.round_figures(T / 1.5, 2) L, acceptance, improvement = run(T, steps) step += steps self.update(step, T, L, acceptance, improvement) Tmin = T # Calculate anneal duration elapsed = time.time() - self.start duration = self.round_figures(int(60.0 * minutes * step / elapsed), 2) print('') # New line after auto() output # Don't perform anneal, just return params return {'tmax': Tmax, 'tmin': Tmin, 'steps': duration} class MetaDict(dict): """Dictionary that can also store metadata. Useful for images dictionary so that images can still be iterated by keys. """ metadata = {} andrewpeterson-amp-4878fc892f2c/docs/000077500000000000000000000000001332417112400173765ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/docs/README000066400000000000000000000001671332417112400202620ustar00rootroot00000000000000For instructions on how to build this documentation, please see the "Documentation" section of the file "develop.rst". andrewpeterson-amp-4878fc892f2c/docs/_static/000077500000000000000000000000001332417112400210245ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/docs/_static/amp-logo.svg000066400000000000000000002031221332417112400232600ustar00rootroot00000000000000 image/svg+xml andrewpeterson-amp-4878fc892f2c/docs/_static/animation.gif000066400000000000000000006323521332417112400235050ustar00rootroot00000000000000GIF89a Xõ& 1"""+++444<<<MiCCCLLLSSS]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””œœœ¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿ!ùd!ÿ NETSCAPE2.0, Xþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏž;:¢sh%Ÿj‚ôMSœG<`@„L4à@#R©€@"PÓ§MG¢ö§ ` ˆ&¡*"ÉÓ3J™®5ó×Da¨EÐ@ÂÊ&qPõA„Œö\ÜdÉBHþ+¨[("—„àÀ$ï^ª&S`©i4m‹ìFññW!¬Oáðy´…ÂB¡áÁý\€Üw‡>,±msn°ƒû6œžLvÁʉGÔ¹ÞD ®XP>.$€@Õw_ùeB‹ Áy&d`@U”0ÀCxÀEL(˜Qhþ­åS p…€D|ˆ ›À2V›FF(Î/³hB0Ðq‡ Ÿ`R ±±ÍV[¨µ QøXÉíU]aÖ3ô@›E4|DÏa›ð²sM„šDÑ’q ¬‘9D¤XõMDÔCljµ·šìV¨´L„ãìÕüuÇ“›ð)®? F ÿ!ñ âuß™,›°3«[þæÏí&çý•Î^}& €¶aá–ƒQʺ Œ“λïÀÓ¹î\îžáïq+„{š7'ñØ– f ˜@©ßÄfŸ×¢oq±Ep´x~lÝ ¡ûÌCŒž]‡?©[DÏ*cÏÙ>güQŽmCÝH¦:w!9BxŸT£¬É=‡#—ubà ª¸Œ1EðŒèWÝ=Å3HáÑäÇžA„& !êØ¦¸P0¬Bì6„ñ)D𠆙¯‡@[p-%`&vyfÖ÷½ç…®r:Yuµµ¨…‹³ºÇe¿ at!L¢~Ì»=Up|Ñþ-Ä“)¦±;…y̰>q½)PŒâó$;¡ŠdL¤ r¨CƒØI9zz jöÈ"ôi (€›õA†ö[­Š€¥Ïñ(›´S ó”Q!4,á&oU„b!*ÿ2Â÷¸4T2 f ¤©Ç ~B0á`:™„èÁ0˜&Pf ÛÃÈF„>D'“Àe?Jˆ 6P±$/ Á{Ûzüc,l’ªZü‰ZN7„tæ= k'ôÚ“:[ñŽi<"‚ ¤#˜§™´;Ý®§Ï93Hé›4‘x¼˜Y¢ÎÛÚ¾%åTÓšyªÃ”€þ)a@n¦þ3­AAœBÐÀ  m[“ Ètg‚¹èô9lãPpy‡>ð8Ébt-Óƒ¼à"ð3t01M D‘ +%jn^Üãíj¡ÈÀÐwÕ"”ÀØ€ž#X“ 1$£2²USÁGá5ˆ’k„ËÏ‘­–á°ÉZÒ; I ú‘ àQ7bìš·€Âñ†XËåH®¬‘)€+ÐïžÛà€„u.Ý”šMi˜¨É`ö‹4o+Íiþ—lGÑ”´…0΀àÔ¨NµªWÍêV»úհ޵¬gMëZÛúָε®wÍë^ûú×À¶°‡MìbûØ»þÀ%í„îÍDÀ€œ{Èj[ûÚØÎ¶¶·Íín{ûÛà·¸ÇMîr›ûÜèN·º×Íîv»ûÝÞ–Ö4¶S«$€y@€6µdûûßg5Nð‚{àO¸Â{ð…;üá²îY•¡!m˜ú¦n^ý ,e*Aa®†ž©1òi”< §Æcb'Êäf¼å´QCNrmœ<7FÊá±ókäü?wFЙÑsw]ä6O:6ŽÎ¦OCÞ×€º5¤N §þ«Ãêκ °Ž®kýë|ðº9Äö²Ûìä@»Ù×µ‹Ãílû[@óuÀ]îx/ÃÝ¿±÷¼ûý }ïFàÿNxÑ=ƒ/¼â©xm4~ñwÂã—~øÈ[> “¿Fæ/Ïù•Uþ›ï<çC_õÏ‹þôU ý4Tzųå¦o½ì%û¦·©×ö=Þ_ …•»îºÏ;ïu€ TàøÈO¾ò—_ Ô>øÂþÕ@ XÿúØÏ¾ö-PéC?îÃw†Âª¿ýòk¿ûÀÿ>ÛÃߌñ›ÿý×G¿ú Ï~¢Sþð—ÿüý^ÿe¸ÿæ§û·{Þ×u÷€å'€~xþÿ‡€ç×€ Øiý§ ا€hv˜ ˆÖ§vˆ ‚"8‚ZW‚Çp‚˜‚*HX.0ƒ•Æ‚ÆPƒxƒ8¸h:X <€>øƒk„Ä0„øW„FhdH8 J˜3Ø„ õ„Â…ïÇ„TX…S8X€]¸…Õc…Áð… †b0d f¸}Z˜†cˆ†àІ˜~phMkø t˜}ox‡t’‡¾°‡(‡~ˆ€Ø ‚„Xˆ6qˆ¼ˆ!¸ˆŒHޏ È}’8‰2Q‰ºp‰}¨‰7Á‰¹à‰™Š/!Џ@ŠvhŠˆŠ· Š¬ˆ‡¥èx‚Šþ¸Š±г˜ °˜‹qˆ‹4X‹¶ˆ‰ÀX%ðk¹ç‹øàŠ¶Ð‹`à{äVŒÊ8ÌX ÎxÔÇ|ÚÈ|Î'Óˆx»Hyä7ŒÄ( Sä‰Þø Žš'Œ¶ø‰RpŽéXŽìØÕH ×èô˜Žòx¶·Žs(èŽGÐäø9}Éwù‚ùq€Žþ‘ é ù8 ûØ9Œ y‘h‘«÷6(’žG‘ i’ É‹*Ù{$Ùƒ-Ù‘ñØ’+i ) É2Y Y“iG“â÷’D“Yé“o”í'”KH”(é‘J‰”.Ù“‚Ç”RH•M°“‰•R‰‘QéþV™…Ni”_Ù•öÇ•´8ŽP‰–J •%É–fÉ’pùŽj9“syEY‘w—Õp“±“[à–0¹—|9’„9’uɓ昗)y˜…9•à˜˜[¹˜Oi—iteéa †ŽÙ%Œ¹–—Ù~ €©‚9”š`™šÄ'™oI™dÉšªi‚™I›®9˜°©—³É‘›¹Ù˜»™£ù ¥ix•©˜Á’²¹”·‰š¿ šÉ9vµÙ‚›y†©yšM¹œÑY†Ó¹ƒÕé†c©›Ûù“Ú –Í™Ïi™ãé…Ý)„ßY‡é‰œëÃé ʼnØy•ó™”åiïÉ‡á œû9ý©™ç©Ÿ˜÷þ™ê9 ^Y ¶é› zœ“É  Ú›âùŒ *Ÿº õÙ ÷yù)–º¡ÙžIøŸƒx:¡$J‹#ê*¢ñÉ¢-Jy/êž1Ê™3úš5*—* *¡<Ú£ïx£'š£Ö¹£¸I¤Eú£Ð¤ʤ}i¢Pˆ¢·¨¤Î)¥Sj¤UŠ¤à©¢Bº¤Zj˜Nº Ø¦Y:¦ÒС¬ð¡V¢:ª¦kJ¥Wh¥êˆ¥è)§°Ç¥uê¥ð¡Qª§AɧÜé§ ¦*¨gY¦Ê+:¤Šº¨œÀ1<ÁAÜ C \­G¬ÂIì K Mü±)L¾Q¬Äœ»/ü¦W<¿Y¼ SÌÁEœÁÅÆ–0ÆR\Æ(üÄXŒÆ™ ÆZÜŪêÆ` Ç;¼ÅæKÇ»jÇU‹Ç˜ ÇœPÅmûÅ Èi¬ÇÇËÇÈjÈd‹È” ÈbÌÆ0 Äœ’¬ „Ü–ŽÌ¶—ì™Ç”ìÅ~üÈŸ ¡È£\Ç–|Ê…þÊyÌÈôZÊžìÊzˉ,Ë/ÛÉŠlËVìÀ´Ûʳ,̾ ¸,Ä«ÜÇÄ\Ì ÜËõ«ËâËË©yŒ¾–ŒÌ,‘À¼»Ë¿Ò¼˜¿wÍÑœÍÉ»Í LÎÕJÛ˜ÎÈ×àÌÍâ½ælÅ´\·ÚÎIpÌ‘œÌ<Ï| »öŒ—ÐÌÍüœ¹"üÏH€Ï“°É÷Üͨ[Ïý¯}¿ñ\ÈýЛÑ­©ª¾glÄ}É - Ðýº©«Ñž7ÑÒ }Ñ( Ñ*=sͪíÏ?¬Á=Ò‘PÒx ÓÄ‹Ó*ÍÓ¨¬ÏìÓmìÒˆLÔàÓ Ô7Ò3ÍÔŽàÔÿ Õþ¬Ô€LÕ`Õ‹Õ8ŒÔÍÕŒàÕ'©ÕOÖpLÖ‹`Öž Ö9ýÑ;íÌïkÔ» × -ÖÿÌÖŠàÖF…×f,ÔsÍÒ¬Ö_mØg­×öÌטl×Ñ ØI­ØíÌØˆà×ò{ÈQ-ÓCM×ÿ;Ðå,ÙûŒØYLÙ‡`Ù'ÔR½Ù„ÍÅ¢ý×­}Ù¦<Ó4=©žíį}ڲ홫½Ç· Ù• Ú×LÚ†`Ú¾MÊ­ÄÂýÊŽMн}ÜAœÜ„@ÜÍ ÜÌ Ýƒ ÝÔýØÎ­ÃÖ½uËýÙrÙá=ÖœíÁßmÛÙÍÜémËÝØ=ÞÆ½Þ®ÜÞÆ|Þò<Ýð½ØåmÂø-Øâíßä½þÛ‹Üß©ØÑôýï àñß“½ß.LàšmàŽànõ}ßò­áÎ ÞÌnўѲýá}°àþÛÜÞÃ!ÎÉÅÍÊþÉ&vö-â3ãÛ=Ã5ÎÐ ÐpSc€â®â ®ß®$0c‡ç.EFNâqä¾äy0ásxàa úâ/=âÛ=žà7‡ç€S>æa½âÕÝâqP(€>°ægÐæ6Õ9ÉeŽ00‡wp&å7®ã}nÒ1~åÀp³M!@±ØæŠ.æ{^âr±‹1t ké?齚۳ à þÐTg袞։>걎ÇŽz^ê|çÅ\ëwpëךÙT®ÚÀ JÑÅÎæ‡æ³~Õ;¾Â¼^àÑîÍU\TäÉ~éË~ØÛÆÏ®$p“f¢JªÏ ð% h¾^½¹nå¾év°…®22«JÀqSÖdãJê¿þî)¾×ò^wþûŠ÷&ù¦°þîîoï,ŽåÐõžÏ'p&mçÑîû ìn>Õß € Pò­’QLpñF0¢ö/?Àä/ñw@òpó ·*_Bš&[/Ð1샭®ÝMšuA/è-óJ®®çn+-±þ¯ÎìÝþÖÍ>Âߎq\ÿ€q_¿óDFÐfÑvA “±Û±ôLôÿðÅL²&‹²»€m_ªßd Oõ ßñCÿñõ@͸Gßî†ïõˆoø_ß5{³9Û<¹¡ï‘2qKÄŸéöãFóàÈù‹°´kÒ´²c#17µ}ó÷íˆÎêœÎìÜŠ!?ǧ/ô©ïôí¸è,ñíD@Òíñ±˜UÏíº®ÞÅ/œ¸Ýˆ±¿#ð%™“ˆd•ÅMïöòÛµº/@^ç 2âŠíߨÇÞå’¸¿Û/:´QyŽìãõWïÚóÏ¡É_ë,î¢æð?þûLïñ@`‰Ä dR.™Mç•N©Uë›ÕjA€J6"·eóV¯Ùml7é–ÏéÍBÇTÀ(5‚ú³‹Bl°è(Ž®ë‹pÑð/q‘‘ìo’²Òêr°ñ²Óó4ÔN´´²AÂ$"àC ¡3PsSÑkVŒÓV·°Ö8ø3³wìP9YyøwÙù „ä„a$@°ØØñ¶{HwŽ8|ü=ݪ¼û\ý>ž‰T¾>„‰¤„™;ܽ]±löXa²‚¸&tøÐ=ˆèÈ@B”¬~þš¹Y8«aššBN4Yi$¤’'Y¶,#Ñ¥° hà”Æþ~+ͤŒtLŽOBeû©Cƒ²näH6,¯±ßô¦Åû°¤¹Ô6,ÏíaJ!$ {IçÞ:ƒÃÝBŒeÅM1ûú»´³Ä¡ÛÐ@€œ·ÙÕœ¥³·Éh&]6¶8Û·ygÜÝ;‹ÖdбõÆ×ohüݳ9ÃçÀ]沜zvÓµK ½P‘ýòuÍýlßÚŸ»W}{ùhFϯ‚Çùõæ•Çg®Þ9öì›Èº»<Ð\F¿ùûJAè”.Â{$þÿ,ܰ‰ú8T¦A ”l—ó*ü0±CqCY &DøNLÏÄË{"5ÄAyôM«KìoFØ0TiÇKѱH%ôÑIä‚Üo¶ e´‘Ê(ãaRË(¡ì¥ä ÄÒÁ*kó.ÑäñË5g›²Ì!Ç4“H7S<ò';_lSÏíà‘Î9å$±O…ð*ÉBAãSQ5b̰IhMŠL@FÍK{LTSIÿ¼2PBƒš”°H;ÝÎJHO}’ÓU§xIS«È”R+kÕU Ï̵=Fyõt×ZSµRPEnØ<ÕÎ×e_STTƒ5YDeu–Æ:±®ÙmVHa§•Vþ[o'¡ÕÜEqÝVe —ÚqÓ%WÐy7ëÖÞQÀÍRÜrû­7ß®ª¥4`Åðµ·]k‹U5Tx vÔÖÌÖ}¸žƒçM˜àwù×_ŠEŠØ3³8]ŒK]˜Ø†7Y׎Y†ŠdsM¾õÚg>Yå8_nà™ŠÙÛ™%®Y téÖçÿäM:& ÙÝWgŽ{xi¦©0Új÷ˆjKÿE:¯ª³v dÙÆ.Êil…9g¯¥ûlLÊÖm븑IÛÙµÍþÚaª]¶[î›i\kÂõ¦ewù^™pÁæ¾nâÆ}«[í®]<ê£û–| ¬9¯8òe‡œrÇ:q…?ÿöt¶WwþoÑ-gXãÌýžzuÏ_O3ôcG7°íËßÞüsÝw¿³tÙ?¥s·5g<÷Ç?ôä}Ÿ=åÚ¿nΧ^´Þý}ÅY¥/ÿêóÅÏ÷{ðA\?Wòáoÿ(õ­˜~÷1…ßUùïW"/±Ÿþ¨¿üðoUþK]Æš'¼ÝЀ“3ö7<è­‚cË`™ñ¿øQPuÚs ÷ˆç½ry\^öÈ< Ú.z­Û „ÀS)0x.| ‹wB†Ïƒý!_¸½°ý-†bûá’lØ)ŠP‡$¼  eˆ¸%Ö0ˆ "ΠÈÂ"Ž0‰H¼"³xÃ-.‡^Ü¡{XEÒþñ}etâQ—Æ ®ŒmT"‘#GM=±…j”"©¸G>²Æ—ä£xDÜéQŒ‡ì£ ɳî}ÌÓ£Þ%9ªD6j‘xl$}¸ÉRvrKg8/sMPfÓŠ]|e7§øMi²s6ãÔS9wYIž“€÷Äg­ôi'~bÒŸí¨ûÖ9PIÔM}fB³EM{.Ô¡”ˆÝõV8OsRÔH5¥H3šÏfZtþ¢Î¬I“ÉÒ’zò¤Æ\)H•¦RÀ5ô¥Esç-áùF;†Ð•@=Ns*7ˆ®I¢3µé?ij5¢ÕqGESR-éÍ”¢t‚.…*Ĥ &ª2u© m*ÓžºÕÎít˜=Õ$#¹ùѰž­¬fÕWW»ôU±¾5¤cMZ\ͺÑñ©µ—:Õª[±zSʵtÕ’]+*ÓªÖ³q|ݪ_yÅØ¼âµ¦…µ›d¡JÙvT¨Ûôh?1[”€µ©UíjY»Úã\h°ˆMlL­ªTÍÞõ¶?³ÆnyÛ[ßúV±æ‹­l\'Y6³Žkn—ˆ <ºÑ•ît+@´ÚV¹Ä}–q•„\I ÷ªþÙÕ-xßG^í¦»?òî:ÞÚ⦽åÕëyxÝ}6šæ]®x›_Cé—¾F¥-d±ûÞý˜3þ½›‚ü­ô² ¿èdpc<Þù2À î} ár1¨D< gåZ ³÷Á8Z¯p/,Ø›„Ä!ñ‰:`B"´´ìÍpw|ÀŸØ³B­ˆs9JÒ2×(1fëhi,`ÃyÆ¢51=o¼%K™ÈMÖiŠ÷äáŸf™°üMò„qåŒY‹PFc‰Á|d1£Ì>~1qÑlF5בÍQ.r[ßbf*ÇRËQµ1 ÝKàÇVÙ,~s…›\ç9Þ¹•Kžò¢ ³83ñÇþvô‚JÀÒxÂ|‹‘yx9Ïk–±ª vißdºÁ›FР<x‚ |µŽƒ´6÷ÌäB#¾°Î‰«cÍaít ×KXÀv]˜§OÙ6›sŽ˜½ìlŸWÖz€–À|À ¤þ̵µc»»-ÝÆð¶nû àL2à^Såâ9õ°ã¹j<<Ò#–7²è-W{ϧ `°‡&x@ø@×—à>MõÁ%Ýf¶(ÚÍŒ¦ñÃåñ‰W| "°É†C¾V,ëùϺ<ùµñmí²¼=øÖw¯©ÀrŸµæ%›Ï×ð„ ÇHþ²³³ó€0AÝE?z} ¯'àÎJo:¼Û-õ¦ñœ5L‡êÐvЄˆ¶¨MÀjÇ|æwúYÕnº½ƒ®ïE»lÞZkÀ¸Öµ p ,ð:þq}%=¿×WàŸ>ø0q~²TÏN ¦€P/¡ñJˆ€æ2`…æš'1Ôkú/ã<дÎMèW:Á¶yÂsïäÍ¢áÄι‘w.üä¼ød‹;ŠØíb²[ßìîq~Éqý³NÿCÕ_ºì=»ò[sû%-|D‘Ï}Vßüý†¡ýï‘ûõ#µýð'¹þ‰?²ó8þ¼ïÔÀCÄoì²Û4oþKþïöPë‹[$ÿŠmÒRø,ìúzN_êþ¦j—Ï¢îÍÎ.ý(Ð伯½ê)­þpkµoùOùp®vùz÷èÏ÷’-M°û0ëj-°)ìy¬AÐØ¢/kEPå¾+ ÑaþmíOôTó$LøªpÁJP +³P·÷ϰÏpøfð›ÐKžoPs30í¦Ÿàð¸ä°éÐ íÐÒÀ¯0Ðø°»ü0ñÀŽpÌ‘ QËQ½±‹ð²%¼Ð£PÓÈРͰÑ0 O0ÑØðùDŽ7,-ÄY‡ü 1|dþqCA1E± ¡OyQþPÑýH‘)Ât1m. …Q N«µšÑAൎQì,Q‰UÌ;Ñ¥¿å·¾3¯?OÙé»Ìƒ±ÿ QÛ‰¨ ©Ëº¸°kq ‹ïYäýn7¬ Ù°3ñ,©/“ËßM±o ç­û û0{QÕÑ ¡pu¬ÿЙp"±"“q#±Rr!½­#—© Ãï Ur#ò!r%Ë®&50"C¯dý1ƒr'ë‹=ò±°'7e$KñíQ Òñ#ÿ«%é% 0&r&p(þ7O(R'Á’ÎB²—’Q’Å»RÉò‡®RÍÒ$Ñ%OR&Õ’#Ý#}qåÒ³r-sò ³-Ù’SðŠÒ%áéR#õ’&oÒ&·')3, ³Þ¿R3²+g-ÿ’3õQ0÷R1Oí[³*Ó2%Wî4 2¡²)ò)q3*/161í5­q6a²6uó6¥ò(a/µR2Y’/Õ/8±r837¥s7‘Ó.ó²7_í7; 0}25'Ó2+s9/3<ǯ9I2#o1)Y%:y393 3>ó3Ño;%©;•Ò3ç31ë“ïö³<óð<*?Ùþ@ÇS<³<Ôá3@ ï9çÒBßS5ýSCCs4ÑÓ/‡ñC³=­s*Ýó:•óBÉ“A4C‹ª@'ô;1”D½ÒD]s@ÏòEqE½³Eï2F9´Fé“?AóFßRB£'JçƒBÿóGC4IG´Ií3H÷°HõóJÃôFE“1atH'ñK TL{”L±ôLËìD 4MuôH±SDtFôNytK4=ArJå£JKtLÒPËÔEMµWv”Fó´AÙôP7TK;t3µ;ÕG H9•RñtOé+GíCSÛtMû”P{J†ŸÑUSþ+áaT©ÔQõ4KAõVùÔRTUûò"e&ƒµ·ZqVµV#U7UR;µWIsñïãQZŸk¢XµNe4TmõMµ5Wu[33YûQÏS™åXåsW+ôT×ÕJŸÔWK³rʵ¸l°[•RÕuIû3YÒ[ï›è•9Î5;JuRÛu_yu\ãõY=P`kìÉÜTQíô[¹ubÿõb}³K òaQÌ^3¶R–]=aó•ã´Ë:Ö|–: –Yûõ`áÕYçÔÿT½>v:Ídq`yvguUdý4Eóe¹ g«I{hM–_Ý5UvfY6ø>õf#¶dÃU_þ™VaVY¡E•klv»Ž¶8u6gõl“ögÁµbÅ•kÉ•jÇÖjI–n—UbÓNÅ]õVúÈV #SkGÖn¯¶mm´Y!Œo/n\ön‘–b•–m!—á6&W÷ævpëf_öm–fÃ_!öø²_±–I¹ÖTeökå•h-WÐ0ws·lÑÖq1oåŒr±ÒuYÑo•pM·iƒöiQ·h·pÙow°w!Ô6ÿ¶y}÷y™—8W¾rW“÷û–÷O©svÕövK÷xƒ7p…lŸ{!P{‡Öx%Y‰—s…wËw^ãvËÒW~ë’}³x·~C7dÇ× Ï·þa×}e—z¹÷€Í¶v5Vu±Q€¯5SÓõtû7f3÷p™2…è?×XI÷÷Wpc—pó÷dø^Š÷˜¤¶7w„×Öb¿÷ƒÃw1é·`QXœì—u××…Û—‚߀ã7‡¯gƒU˜7XXs ¸…a8•L¸Õ†‡6„‘øˆ{˜†õW†UЉ ˜Š“xc˜„Å„É7ˆÿ*‹G×€¡wzÑÕX»7o«˜[l:ˆø6ŒØ‚ExŠxUÙGŽˆŽIÃŽñøŽ¥ø‚…t‡;¬ŒW‚ÁøŠßu·¸ Y‰y*‘áj‘ù÷‡S÷‘õØsÿ˜áøu͘‹ÝØv»Øg'9r¹„Aþ¹ˆûx¶´8“+Xg¹s£\Ù\ŸXó8Œ‡’ Uÿ*Yƒ.9Š™–‘¹–WŠ…˜³& 9™cyƒ™XážÙ©ŒÙ‘§¹—»Vš—ƒÙXƒYYya¹›5™›Ù—9Ù–ù›ÉJ›×ùœeù›áw—ëÕœÕÙ›•ÙŸ©™œ·œS6 ÑwŸ¿“çÙ‡º û™·¡!]=ø”Myiz”3FòYGßy¯âÙ¡1š—ùÙ+š”K¡U_ÕUc•w€­eÛ®A»¯U-¸Ó˜¯½Ú·¡ ºµZºßØ_¶û ‡{Y `^.¹—`¹›fúòfû~{›À¯»º”ýÚ ä{ºyÚ»ë¿÷þ˜?†¸ëáèz D€èXÛµ•¶›À¹€Â#\Â'œ& ×TË6@Ã7œÃ;ÜÃ7ÜT‹Ô&€ÂKœÂ-|0üÃWœÅC<µFÜÄcÂQ\µ€Åo¼Ã]µ`\ÆcœÆS+Ãq\Èux¼ÇKüÇQ+È…üƉ€ÉqœÈüÈ'<ÉA`É£üÃ<˵<ÇEH¼ÊOüÂSËÆ½|ËW«ËÑ\ç<ÌÇœÌSÈÙÜùœÎ¿üÅãúW àêÊ­#;²'» Hͨ ÝÐÑ]ÑÑÝÑÒ#]Ò'Ò+ÝÒ/Ó3]Ó7ýÒ ÛUÖZ¯§í àú暦7þØTZÕWÕ[ÝÕ_Öc]ÖgÖkÝÖo×s]×w×{Ýרqý6ÀÔŽeñO p öL ª§ºª{§Ú=¦@¦Qš½¦? §«ÜÃ]ÜÇÜËÝÜÏýW`º"ØÛUœ{XW…ôh RaŽÝ&š½PâÝ1è}Ù{ ²E8 €Ž!ì½ðýe˜ZÙe­1 Õ?€¥E`pí˜@æpš¢ºØû¤â/2~  ~ µ,OOlhb5Bà¦= ãàãEÚBÝ­se­Ù=WH`ä™ à8(.:èI~ÙûüT>¾ ¼þMÀˆ^ dîèwfÜ[ÏÛ}½±%é™ ˜[ÚþýR¾õ˜~© @ Ò{ì}æò-µœç¿ã;@Ú]ÅìM@µ™ £ìƒ¾äé~ àæ/¥fÎÀW[õüžeŒ[ {|"@ã4À?ððMÀ¹gZ²)[QòÞ2<ã@mâ e8’`óÝó=ÆFò±åéÉ~Uò~õ— ó5èùÝ Ìí¼EøM n_ rÿeP[ öM¼¿é3ï— ï‘>óŸ  S‚?¹£ñ}F±ýüî—×xôÞ @íu¿ú 54%ø•^ Ì_ÚÒÿeæn ìÎ[H`þàóá英h<"“Ê%³é|B£R# pYr™)·ëý‚¿!²!(Gä0»í~ጀÆópâú=¿íp|hxô"&uph8pp ‰ tt (rv²1:BJŽ dxtH y¶º>1€||Ì…T2fn¾öú"–@ äý##a4€$KûX_Ü.4ˆLƒ'V_[4H°†»Ç-_…l?¿çëïó÷ûÿ (p Á‚"L¨p!ÆBŒ(q"ÅŠ/b̨q#ÇŽ?‚ )r$É’&O¢L©r%Ë–._ÂŒ)s&Íšþ6oâÌ©s'Ïž> *t(Ñ¢FBÜ` YûDØðäB¤V¯bÍʧJƒZ#¢é+‚W“´¢M«vm’x^µÛc–-ݺv‡: ‡ U‘ :03 ÁÈHp”Ü€5á @½J&øÀ q¨.Tóu°‰T!ð€•¼w_ÃŽ S„# B\pJ¤À<45d— [<æÁ%Ç!ˆÜ)€Áƒ!˜‚BšÃ 8x¡Fª‰Ü.xÀ0ׄ[Y·eÃ/?¤‡·¥u›`±‰ÜM F–|£FE0€ÐEeDÀ›†Q@Ý 2Á•~G¸X¢”õQÄ\cNèA0! H„$V’5™ky6A`ÉÜŠDÐ"†BÌ—°€P5¦NS0L1·çð**@ŒFà€bBñDè…äŠåµ^jeЧTµ)lŠ&¶@€:-°*Pµk¢,Úè¤L *‡¡–³¬F ’ÔZ ßþy°™â5û¬¸»ÆéÐ}¸.QBbñQ‚‰µ áÁ~š°Á²Hèd€k÷]fs XBm“ªHá¤î¡µ–ZÜ)À¬X§Rê/¢j¡N`0ÄŠñ'ÄWÀWG̪rbuÖ™fPÕ¯q1o,—v0è'·7£\r  Áœ[‡íZ!,†pÓ@‘!ÖOû)õR„¡ôÖ ÈÑœPÀa­D ÈÁHÀ6¥†j÷›D0ì´Ö¹NýSwì¤ {Ó¬Ó‡— ø’5i–ÞUøÖ½Ÿk®ÀÏ:yLĬm&Žx•¢ž«Cìm¬~§þ“þÝE Z“›0ëˆóý7ÅÇ—ûîfCDï<\ÄTw#Á%À}¸µI`ýÎÖ`ÂðEƒ[—,DäZ<8eÿ´Zn_h‚Ä¥°%ÔXÄ\˜«ùO:[_z‘ú¾–ÍÕÆ{¨€vt2?!Le‰A  J :‘Õ&<ëLöä·¼mO€ôŸûR×@<°xA[’×ÈLÏy€ÞÓ9±˜G=Ó@Ä',.CkÑPîs>\Í~³Ð|„€¾‰%á343‚͘Hó˜€ ” Ø,3Ä à°ƒ|i‹u ¨@ YHd4¬¡ÅèÁÄäPp[,ÎþQØã1A?SBžB¦„€nI¨Í ‡p@"PpR¦*Bà˜% ž/}I Za“Èﵨ-MŒTîˆð&0_DÀb»‡#H !,w*d®Ê¸Dzðƒ¬ß"H%´rŽªB')Çб!*LB ð¨¼"ìësMø 6À€A!OÍ#‚ëúVAÿG'Œ4_êD7„Y).xóñ°)»"dU›Lt¤¾qî•ü™Z¸)Òõ.ŠÂJ>Ó:ßÁn—k¦ÚÂÏÞ½naT ®ØøK†ØQDìdY•³ö%A™§lfÃÎ Z±h £Äô%—Ò”€[³ â`Îþ"™\¹]ðNc"‡që’ÁÔ“ðt”<°"Îà™Eí¥1Eèf¨¤ï„…šH:¸EŠÀI%ª}U‡}fR4¥È†PІ„xÀg àÉ$Ð.ƒÂ`ñéÀë0áKE‹°ªÈ ÀŸÊe€ke ‚@àC•:(à’•§è3$á Xl…FxE±bIs ™Mm0¦¡ „½A_W$ ®1}GÕR–LåOÂ6êŒî)A_ð×¹s¬uíg² ;ë8@3íz*QÍz8`+h€ö@˜DÕ`™bÈ2© ™@»«Ψþ 7CB—¹@`We²ÒgM€èF ä™6ã¶+»âeêË \ §Å9gÓY<—+tùY(—«;ÔÆÑ¿G*Âó Ü$Ê—¨ñ=–œ²Þöà½BYé[%Ùo)K›JaæR¡ÈÀ8À=PÞkð·„*'ÍWÎ|E’É D»!`û«3]6%GÎ °Ê­Ò: ø-ÂLýHØ#òH 4½ìõËm A<©»2 ”!É/G„—¥ÎpˆV•Iê²zЈ†Éwu„‡9‡N´¤Uò?SÈÎú#Â2?‚þN{úÓ µ¨GMêR›úÔ¨NµªWÍêV»úհ޵¬gMëZÛúָε®wÍë^Çú(&4X&>#yfæk —Íìf;ûÙÐŽ¶´§Míj[ûÚØÎ¶¶·Íín{ûÛà·¸ÇMîr›ûÙËøH?2`º÷B€˜³_ÛûÞø5òÍï~÷zßþ¸Àg ðüਆ‘¡¡púœB§Lj wÀH 黳ñæit\‡Ç ÏtNâ ¬îuA9rŽk#äЀ¹3Zš_CæÎÀy3t¾ ›»ÃçÕàù2„® ¢#èì@ú4Ò] ¦SÃéÒPº:¤>éªþ ‚êèÀºÕ·Î­›Ãë\»ÀN²‹ýìp0»8ÔŽö¶«íà€»ÛçN¹{ÃîtÏ»ðÎ ¾ëýïXð»6øÂKðØ@¼áÏÅ[ÃñŒ¼ O ÊKþò–úÆ/Ïù-d>Ÿï|áCÿ Ò‹^ï¦oFêO?÷Õ÷|ó¬=\¯ ÚË>ì¶?:ìoÏ{í¾¹ïý¤ƒo â ÑÆ'Fòÿåå ÃùÌg.ô1ýèÿ²ú¾À¾õ‹§}^tûqú¾.8VoY/üÃÿ}Ò»Mrôs]ü¹àØ*@ÿúÛÿþø¯Ôï~«ÃC0€X€x€Pü×é×þ~뀨€È€’ö·¸Hˆ{ ˜Èè¸uh #H‚‚'Ø|.x+È‚h‚/Ø€ñ0ƒ4H€6xƒ‰–‚µ ƒ;˜€1èƒ „´ „;؃FgH8 JHƒLØ„Nö„²…,8…T(}EXXH‚Z¸…GØ…e8„5H†bXV _Èa¸†(Ô†°ð†‡rh6tø vX‚j˜‡8±‡®Ð‡ˆ‡€È†„Ø‚xˆá—ˆqg†h˜†èˆÈ!ˆ­°ˆhˆ–ˆø šH‰HG˜È ¡X€œ8Š5QŠ«pŠ<ø‰ªØ¬¨ ®Ø°þ‹+1‹©P‹DX‰¸Hºˆ ¼˜Š¿Áx Ãx‹ÅxÇh Éè‹ËhŒÊ¸ χь¥PÖè‰Ðè…’8‰¯ØÛ˜‹Ó8xß޽8Ž7¤ êŒå˜ îøŽ2ÁŽ£0ô(â8ø˜.a¢ÐþÈ 9*Q p‰ ù Ù&ñž‘IÙ y‘"‘‘œ°‘ ¹ ’1’šP’&É(™ *9%Pkç·’‘¸kwŽàHŒTÀ1Üf“4iŽ>‰gˆŽ:9˜H™û”?y -‰ /yxPèh‹LÙ”Õð”—•QÀ1TY•þE‰•N‰‡““–]9•U™ŽbéWÙ \9{j –dÙ–¯÷–}g–hˆ–rù•DY—v™ Zi qù^¹–|˜"˜Y©—C˜˜Np˜t‰—ŠÉ ƒY …™sù—”Y™µÇ˜•ç˜KšD ™œé™5™ƒ¢)…¤9¦™“­‰š¿p™”™Mðšg›²Ù ´9 ¶Ùx› ›¹›ÅЛ’ð›K€›{©›Ä9~Ì9s«™…º©œùœÍ™Öi™Ñ †Óœ¹9œ× Æ È©Ô9šàž³™¯7”Â)çɚ驞¼ÉžŸéžß ŸÞ¹œóIŸ»0žPž¾ç—ïéŸÙŸ¡‰þŸü©ŸšŸš: Šñ)ú AhŸ‚¹pØ º ªy zz‡Š˜¢»˜¢Ç0¡GP¡Ü9¢*Z‡,Z|%ê‡2 £*£3:ˆ5Zœ7Zˆ':™=Zz?ª|Aʈ Š¢û³5·²¨È±ÛZ´Ø‰³«³ê*´%{§L €[ŸP°< ¢þU«‚Wë}HŽ[«]›¡NK}ak•ck²e …_ûŸiË–R´m{…oëœY«¤4Û±uë†wq ™·9´QÛ·4z¶ë™·›£„«µ†ë·ˆ›±ŠË²s[³›‰kµ“›´Œ;µ{¹>¹XK·{׸z º˜+º`»¹b»·K‹º´˜¹ØÉºjëº= »±«ºpK»rû{º¸‹¥@k¹]𻋼Oª»xKºÅkºÇ‹¼Â{´¼+¸Àé¹# ½È(»^;½J{»ØÛŽÚ›¡ÜÛ¹Ìû½¾I¸Ý˵æ{èë¶ã»¶T۾€¿¶Ë¾ô»ïk·ø[¹|»¿Ù¿~ûþ¿¾ë¼”+Àl¿š[¾ž‡Àœ«ÀçËÀ³ëÀZ`¼ ,ÁÂJÁÛkÁY€Á¬ÁIÀ4jÀ¥k½3+Â<¼ܼ(L´*L’$̇êK¾ÄÃËÁâëÁÁ­‹Ã.9Ã>jÂ.ÌÃ@üŠÄ5,¿Ÿ{Ä9̯[Ä7ìÄä)ĘKÄ\Â?LŵiŦ¸ÄùK¶\ì›^ü¤XüÀ/\¸c\Å:œ¾g|Á>\»k|œe»oüÁqÜ»s¬Äm ¿FlZ,Ç{ÌÇPì½R܃¼IìèšÇÔ›Èy°ÈÐÈiì¸<Éu¼¢wÜÕ ¼—¬’Ì”üÇŸ ¡¬È` ÀQ\ʆpÊ þœÊÜÉÏËʉàʵ Ë'LÊ´Üu™,Œ¸|È«¼ËW×ËÙ»ÉWÈz,̃`ˈ0ÊS¬ÌÀ̇à̈ ÍÑLÌÎøËYìÈØ\ÊÒÜÊڌƺlÍc×ÍàkÌ€ÌÍ}LÎdºÎ<ÎR)ËÌÎ~ðÍ…@ÍÁLÏz`Ï„€Ï†¬Ï{ÀÏËÎp,Ï! ÐõlÎî‹ÎU€ÌŒÐg ÐÃÌÐ;©ÎÐî\Â𜖽ÅÉ Ç-Èþ¬¿']ÎMÃm”ÝÒ ýÒC¼Ñ}‰Ó4m)mÊ#ÍÉ:½ÓuÒçÓñÔB­Ÿ6}ÅH]½MÔ¼Ô_lÔýÔPݼþRmÆV= [}ÕpœÕvÜÕ/:Ó^=}ÍTÓÏ\ÖipÖ Ö†IÖlínÝ+-ÆsÍuÍËp­™%ÌyÝÖDÍ¿}=¸ýÐÝЃ=À…íÔkØc°×ýÓÇ,×í‹=œ–}Ù<ÙIÙé|Ø =Æ’½Ï¢­Ø¤ ÖÂ|Ú ½ÙæÙÙž=Ô¬í˰ÍÕ=Û¥[ÛÅ,ÖSºÚºÙ¼Í·M¡²ÜB;ÜÙ˜Ú ÜÈ]®ÒÌ-ÓÎýÜ‘]ÚkPY±|ÁZtÝÅ=ÖÕmÝ«Üs@‰QÚã…ÝÓ}Ô¹MÞ•mÞrÚ³q fCÞ¿íÛòÝÎÀ þ4³qÀUŸÍßd:ÞÿÕÀ!BAÚÓîÞ^§ ¾àLßq .L¤= ßàíß.âÝFpÐ –âñ­Ú$¾à&nLŸQ•at•]á‚záŽÇ.à ÐP§Ÿ<î¨>¾1Ik3IË5^wͶ¥Û“åt0åó{À”^^KyåØýI!)eÎ`B›ä¯:ãkãÞ<æhðiÕVUõ‚älîÐë‹×ìŒåÔ%&L°¥]jªB r±y~ÜÍæìçH° Þ°yBè?dF/ZÞÄ¹ìæŸìèGPàOP©I@eôCýZÍ‹þîéð­êÊ êÆÔ‘N]Š:fb–l‚ªæznîÞÚp~ €Áì•qäF@êH]DpguªëŠNÒŒ®ÀŽ.ìÁŽ ð§ÐjLÐÞôÖ쉾ä«Þë» ëV°íŠôhPZ›~½±ílîU€î¹‚i»Òî)üî¬ÉQqþþðïÙNë„~lB€ëŽJ­Öª¸ˆmÜâíû>ÆÞ ®¿>Ï6ê@®&à§$oàñqýð@ ò{åpòð'òMð®ñ:¯zCJTqAæ! ‡ï0¬ïäå¿;¤´7dè6‚è$oØáïû+ïm€ój ÌÿlÍ Nþ€Y çÅã;/ãGïØ[ÿæAþ#`"˜Ú1hÎåIßõœ-òåÞódàÐón%°ª›ªõù ñ_OÝJO¿Ž^·±yžÙÎÎö£½÷íëè'rE7"êýÞU­ö±Mø¬ìèpp3À;žöyßêwïëaï 0!€@g;øœÿùmúM'% pfÍËúÛÏïn?EÂf÷Q?î®_ø¹ïÄŽÁOá­?üx_ü“ßûbÖ1gx&ü,½Í’ß܆o¾°.kðÝx~üâýüÛ/õÔ_Ñc$5–æêÏçÜÿýßËôâ_‚?ÿTþõëÍ@€LEã™T.™Mç•6K àðL¹J!ÅÉeKeØ +f·MTGÙo»8>×ïù}ÿ0Pp°Ðð1PH.±ÑQÊÃ!à £äÑèëî.ϰnó­³ð. 5Uu•µÕõvo1–@ "„U³Ô”‘´wL M˜Œ¸v™¹Ùù:znVº:©ã²•ïtÔ˜{Ø»ø8ülÜ:]}½Ý‰ú]>q;\Y0˜û^Üüüw^@ äÏ`B5õô¡Ã×ÏÜ>@ùITxcFÍnôx„aE‡üÊÙ9¢I€Y:)fL™3iÎÌÖgÀþŽ93†fÑÅŸ'ÿí”gK!˜6uúªÓ•I©JÛYՠϣDƒ¦l8eɯX«~©pmZµk+PàJî««quãéA¯"ÁÝ;´/]ŒFKå|ØÑ\Äí´~«W,ßC„A^,ò&Ë—9ó ܹZãÊeýÝú²¿ {švŒšuìÒ°e3­™´žÌœrOs=švmu»íl~܉bä¶íª6Þ…x¨Þ 㾤£Bþ6Ì®ÄG´0Êù˜üI³¬Ò/ÿ^S8*ÉôÍȱ<Ù2 q¸3åbs59Y3ÓÎÓ”lM8­ëòÈ<í£ÓMA?ÂÓÐ%÷„¯O(U3Ñ 1Ò‹­t‰+MüÆOôMÓG|l“ÒP ºÔT:ôòQ>9u4UDF­3V¬P­US¿ÌtÒ]½èµÖV –ª[cÍUGaÿû”UbÉÕYžŒMÙ&™ ôUP£}¨Sü¶•¶TS«åòþZHŸÕöÛ0¡M÷ÐpC—T_“˜µP(èu7Ý{åeÅ}…—Ös›Ív`~KëöAƒ=šVÜU±øáAôU85t) ßJ®wI„±TvÌ‹}óxS‘bø]‡Í•Ør]5¹A’u…ÙÒŒ#ÝØfUeNÖåai&qgk>5gCqö7“– Žxh-ƒ&·éPÆôhãìyÙ¨“SZëy¦ÖXå—Y~:^«ÿìÚéuÑvçë›Ãöylµ¹•{í¤É¸î‹´j¬C^z弋˜XpÆöγoÀÅžÛâ¿ãÚqkÚþ÷í¬!œ$ºó&<òt&7ºò¿/_<óÆ ç¼óз3ñÑáfþ¼`ÇQO½_­[ÛôÒc?rÚŸùœïЯVüuÝ™|vßqáÏ&ÞrÜwG¾wå™C:øE£‡ýø°4¯;yë—ažuç=…^t黿zñi!_ÎÛ¹ÇÜûÜ7oß}ñVß|oýÞuÛ£ÞÝ8–¿5a¯yÚKßüHW¿éÝ€û3 uX>P}ôS—ýØÁ N‚¶ë_ÂЇA‚ßá >x&ùOƒ]ñ í·Â]HL/tàúd¨B´ÕІª€Ÿ Gø±ÿ=/€  ƒ(*‚I‡| yøCü5QR-ÌaK–ÁR1†Vœ!]1Ä-^‰^D!¿¸6 ’þ11O¬R{ÈÁ:NÑW„#‡´E.ÎìˆçK¢ —hÇ=ÊJŽR¢£•ôX1<Š‘‰‡üFçøGž•0'` )ÉIŠ‚Tã&Ù¸Æ<ŽÑ“Ye$uÓȃ±²ioLe1(©HK m™„a!ÙÉYzf•¾l%*s)JÞ󗈬¥“éÈ*:3Œ]“e2Q²L"5ó•Â,‡6MjvÅšAÂæ0¹id³œß aÔÆ¹MmR眅|¤:NµžéŒ™>MæMz¶Òž/§9ù ´‚^ÌŸÿ\g,o µbú“Æ”ç3ꛀ²h úq%9ßù³„V´A=QF÷ÙQÆ“þ“ó©yDº!’¾é ‹©Â>ºR{%’™ -[ !úPF®¦6ÝZK1ôRƒš´¤*eçF…zS¢NȨ7Ei)IyÊ™6up8½¦Nñ¦I%î0š"œ*VCL¥ºó¬ùDj?™JVxhUœ\-`ÚÖzÔ´z´­nýÕSU™ÖUªWåWPõª*¾ȯ[+#+ͼÖ°f¥(GïzRÁ²‹°}Üa”XºV6©“ehc5»WɆ•²¢U+h ò’š¼¶ ¸‰ä[Z3ú1”=åW©Ê[–,%*Á.g{Y¡Þ¶’¹ýén V^.Ì.l‘.[ÜBÜϪ¶´Yµn|<ûVÒ¦µþ­ùîûj«Yäʦ@ À%l*$@Bwy5ÞÕb÷d彡}³»YL-@øÀ°%hÁ`2/¹.³vÅoB"<(þf÷¼¬é@À€Ø³ÍªrQÝzµ¹Jѯ…kÓ ƒæ8|?ø†¾¿ºph˱SèÇ}qgp€#„H‚p‹èb¾F`…ÿÊZ¢™Å€Õë9SaÀ@< | ”“Fâ.¢X—Î5e»°,Äo®œ1À—æ17Ah@”Ý H/ÎÅ •SÁh²rù20²¬'0@ƪþ*ô%}ZâN¿9'Ž–TÝ éÅ< 3~ ¦£L€¼:…ž«w[eR×åÖ£®µ:7À_3à ÛMʆÝ[„|Ø ´¦wm·fg5×òµ£}ÜÖ¯ùÐs(ÿõ0»Ïüê.þz¬fÊï“ãD°=4ðç8¶TÐñ$èèOèìoÖêëQ0¿hPÿòô\ðãHp÷ùP¼à¯ÝåpÐétÂ6Ð!¯ KзoΛ«ì 1ã PÉn Ï® ƒ³É 9 ÕðóB µN §Œ p|ÆÐ }жæPîêPõLpY0ß ÃòŒO ï‹aÊgГ° iî -/ ±%, BýOû°Gï%q I1ùM‘öäP÷Ž{0•þosO'‘c‘Çt‘ùR‘yqþZ±LFñ‘ Qu|1ÃüÐïQÿïOQ#:±ŸøBq÷ˆqqñÛø”ññ …±6^Ñ­EÄѲîœQý¸qk±úÀñúØq™±ÍQ6ÐÑ ðõ1 å 3PÛ± KÏ×q»±)ñ GÁOgïî k 5 K]1!óÑ’ݰ!1òü²q"_0$ ²$uì!õg##p!o ¤%9R]’ßï$±Yñ#"©±G’ÂôWqW²+2û`ïêf {²‡Ò(‹’)þU1#Q1&I+›ê#‡*=Q*§±+—ð'É(û«,Ï1'iò%C®-9B)1±#mQk’ÕÑ ­²ër¥â²çò*÷r0Ç2ÃR$ß²oòNs1óºó“'-3+û’0 Ð/ó²(©R ’ Ïr*/²4Yæîr)¿Ò Q US-eó(!3)ó29“,iÓm%16SR#5S'‘Ò¼€S…³5?q8³ð5k2*¿ÐÓñ‘óö>3¥S%%4üÑ;’:“‘;s'ƒQ+×’+qó=,aS/×Ó#™sÓ-·r}S \+¶4&Dl5¡þõx1T@”@kÇ31 ˳÷´S¸**ìAy>·ÑB;”)ZÑ0qR?Ñ>ÕS9I„¦KEÑ¢º¾3(ó0C[$ºVTE[tÅ3C(³7K´2ytGO”.!TòÎ!cÔÁp”3ÈSHÛOBÿR4s“?‰ÈH£ I/CIýÓ.C3Ÿ4>‰t¦”ûºIG5OKaÑGÉTB«óþRL±SLG´8½Ô+åsNåJÝS&ÓÓJ¹”¥Ú>ûóL5´Lµ³MÍ3Mã±PðMÍHMT773ParCïôO¥TO;ÎOé”R5µKÁ3N=µ>Uï.1Hœ2ÕN;õEëTYuU7þ5UótR‡1QCt2á4T59u5H±4B—4 lj?AW]Õ4÷OÅÒWAóP 4-Ù”TËU[V­UU“•DEõGyÕεXøT£ªt1®4R³´I·sV•[ïI\ËêA›•IuBÕ^Ÿõ69•ˆ•\KuL5]·T`§UR‰tùµZ_5[]s[½µGÛ`éUMwPâ6^ÏõWåÕPéÕI‰\§ä]q¤_Ã\!¶`–QL -6;ï•cÕf5Ö"…Õm\vQ 5e£ô1yRMVé–.XV/FVFõZ–]UMcÖc…6.ˆv6V[?Ui™cþÕi¡VkiÖïp¯vamÖj«6W}vWѶ;%Q¹ÖMÃvg—µgåög™6b½-­óXB–DŒvhÿõd·Vg×`£ój·li¦dí6pi¶cÝvcñ¶H!WQá–pQör·pŸ3Z©†oŠj¶lE÷lé6mM·W³ÖEÍ6xfWm5v–qÑ•m¡Uo©åsk.tɶt+un·ng·[‰W×Öõ0]—­Ävi÷nU7r¡·^ñÕpY·y”Wd`uew{i×yWz'¶XÃ{ pm×qes7WY…·’Ê—b´×}Sh3Vz¥YI·u)×VÇó|í·~»öþ~Õ÷vó5V-yáõe×—5·%×€±u_ùw|å’y±6€ç5}6‡·{ã ~iê8ƒ£·„§× ¸zõ÷z)Øo£v„5x€9xfÃ7o×ÔsCØ`äW_1¸vø‡I8ˆW—…ËG‡+†Mxˆ•ø{Ñw†)øcÉÏ…+%½Ô¬½˜€b,` .nç—{ÁØ{?¸i!x…}·tAa«AsŠ#%یÒ  `6À º˜Ù2wƒ¿¸‡ƒ7Œ‚=´CAtw‡*R6¬ÛLàØ” N¡ŽÀ‹ýø‰-¦‘97w#øp§ “Ûwo(Eitºl´>/8HPþí8j ÀŽöÐù€ù§øìŒ;”sy xY“qxy¸F˜‹Y•yä(™Ò, `ÙŠ`ÃøÝ,ù„Q瘶‰x›‘ ›GÉ‹˜¾FߨP¼ ÌÄ, Žàߪ¹†¯Ù˜q—˜µ™Œ™€œó7œc›Ù.™_dÏÀ,þì àãùq.X—%؇hŸ“ÖzÇÖ—ý`¢-z“ÝØb2ZVÙ·ž"¥ÈŽÌ™‘`¢Ù¦ ÞY(À¥_¦cZ¦)` L&<`rZ§wš§{Z§=@&àkfš¨gº¦7à¦}Z©—¨cB¨‹ª_ú¨eþ–Úªyº©aâ©£ª§:&púªÃ:«A`«¹š¨½&À:¬­z¬AàÖúªÇº¬ÍZ¦ÑÔ®}º­ñ:¯±:¨`¨éÚ¨m:&ªº¯õz&øú°sZ®[°©¿z±{z¯'Û¯ú•#à&\–@–iÙ–UÍKÛ´OµS[µW›µ[Ûµ_¶c[¶g›¶kÛ¶o·s[·w;¸PYNˆÍ9 "Y&9 H`l—›¹›Û¹Ÿº£[º§›º«Ûº¯»³[»·›»»Û»¿¼Ã[&>`ä Ž@Û¸­PßêøŽó8>û:盾¡ŠàÞŒ@ßøÍ²xþ½¸¸¾\ÀœÀ ÜÀÁ1侯8QÞù)^ØJ  $`¾à€à¤DÂ)ÜÂõû)fYJ"á߀~0\Ð6œbâX½ ÅÄL¹? ƒD`ìŽ Ð¸˜ìؼ‰äÆsv\¿`Á`âÜ‚¤ >€`‹ ¶Ø~|‚œ_€ÛØ ÆGž1…Šü@²a Èd̼$ÁN´¼püÍϼm͆•À•!|ZÌɼJ€A[Ä«¤ÍÎáx– =¡íÐùe¤›ÙÒ%Æ×+@¾å$Ñï<ÉŽ 8L:ýÍ@þ` @ËGšM@>ÝBý˃e ûLù&ÎL ¨ùLHн–ùÛIHÝ2< ÌØkÖY¤Ü‚½†]a `ÒÙ #¥`Òâ\У=Ì€ÚýÛ“À•³]J$Ö¥ÝjyÕ¿EÒHºÒ1…ÀšýEHýÕ•,ÖEÜÝ\ ²€LÔÝ ‘Lß‹@Öõ<Õø<³Ce³íEHÝÑ ÍË}LÔÝßM ÐƒÛÞT¸Ü<ÌTH`"ý×ËÝÌÑ\Í'>ã[Ö¤–?¼ÌíÜÕ@æ ÓÒ;ÜÅ$€>`×{ÝN:€Ì8€.!Р:È«„èþ¬¾FÀÉ= $€Ù¥¤–Oü –{¼éŸžbê½ÚN0@ÊÌÏ­¡ ‚ßR<ÃYÜIÞÞ)`H Ô <¾>®*2AÅ5<Áñ_ñŸñßñò#_ò'Ÿò+ßò/ó3_ó7Ÿó;ßó?ôCß6àß ^Ý·“à:@ô_?OH €ÁF@Ø^âÝÿý`Ÿ÷ÉÐ߀ÿ€ð¾÷•bÔÝLÜéÙ+“?NŒ¿9`ú3$!0œ’À>@Ê@æ_.  ^½ÒÕ=ž}À¼ŸâÚ‘ßþ'D"þ@ .õ‹ ÃÆÓxL¦PÀÀé0€‘©„<:D ¤t Or!d<D) hØ(/ÃŃ <(0€€x)%*.26:>BFJNRVZ^bfjnrvz~‚†ŠŽ’–šž¢¦ª®Fzp(I$PU ´™4$L™ØE$– 8|Ý)"t$>ˆ˜¼(¾á™Èb$B$²†‹“—›Ÿ£§«¯³·»“»Âšè% ,3( 4(ÊRi¡¤Ä.É&â¢èƒ²7Ä]‹@š€¾L€{§q#ÇŽ?‚ )r$IJÈ“U/_ÂJ°(ÊL€šþ6g™³hA5ˆ2L°Í§‰M¼•±$Ó¦NŸB*u*UMñòà41€¥÷´r Cfš€¶Ha"Wxì u£LìѤJ–VÍ«w/ß¾~ÿnûJ"­°\îê5Ѐ­ qé\ÖìY°7ßÍàè†`&ö-z4éÒ¦O_:)ž’†¿2q¥€ç+æpˆpA‰ZEfШaã&Àc;Ø$(®èÁ€‡PC.}:õêë®j^¹ ±‰\D¸‚ÐD‡ h$­·e>@/Áçå¹zêzÁwëþÿ €v“&˜ ‚ 2Ø ƒ@PGd†ˆ ==˜¡†rØa^pÞØd‚ ‡'¢˜¢Š+²Ø¢‹/£Œ3ÒX£7☣Ž;òØ£?¤CY¤‘G"™¤’K2Ù¤“OB¥”SRY¥•Wb™¥–[rÙ¥—_‚¦˜c’Y¦™g¢™¦šk²Ù¦›o§œsÒY§wâ™§ž{òÙ§Ÿ2!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏž;:âÓRÑ4 9ê†)΋ `¨ ° ÃŠU è(Ó£#‚FqŠ„ƒˆ0¢IØ#ˆH¶̀KÕšaÛ÷éDäVuÀ0d€€ ˜/HÀ¨„ˆº&˜z&ô'Ï Ó4À B„p`r×HÞÔF ùØt™´¾C 6\ÑÃÞ‹™dØY‚Hs!/Ž„º „ Âê‡%µ‹€Å]Ä:ޥјOO<£‡äJJŽdÙ0 ²n}@šH[APz&¼GöõÆžpVù€€Dø$‚~0õÞâþÑ—–jµÀv$<0@Pˆ„NlWÄr€ÀãñµS D8aL€6 }lå§Õ¤¦ˆú0’Hvb…µp„ˆâ‘íE„œUJt@%t€Aß!8€` ¢ ôç_Hh›4àXj8A ÈqHDt ‚©N °@dT])ÜTDðL!w`eL€€šX‰Bà@lä šEäuç±á@¬C¤u*˜ð¥=b ™V¥¸á…›(ë- ‚Ú²¨FàUÐЍ¢Ò}P¨þ˜I°·Í~ À­e6ô­H`'bbåYY—Ùʼn„èiÂÉ¡“©½×£ÙõWlômX«‡jUqX BbÏéÀ¨’ !±œ½†Å™ÄÁ°ò À篞ŒDXši&fTñ ÔšÇÕÜ[éÉ6{ì¨ j2À°#]¤¡ý˜0 ÀÒï9mrP!5“סôï-/Dó2Q(€PB1«-Ä0w¡-¦‰@|Dcd !hsKrô5ÚᤂY§“Bš¸@ Py¨[?}&¤@åØmbðÑØË7óÛe> G9þ¯~y9!Lýy­‘ø° ßÇT¡…›asin‚e¾+>ã%§“ò̯íЙnÃ:Ä€ø[Dc^'Á%Œ_y°I ¾ÎRƒ`ÂìEÄVhwŽ`©?Yœ?¦ ágÆ¡@ø42R&kB(P«uHhÌOr¦>ЄçQ»J²Šà§•ÁF~ûêÖŠ 5! „&(Að'¶5©o}í…PÂ÷ep{ô[Pfh‚©XÏ!m[BcH„ÄÍ,7‰a@¾$”_®ëž Àr¾ùÔoiB ÖÅLcøˆìbôÒý („â¹Ob†9š!MüŠÓþ÷…B³ïá I¨š f&@)f°ÄÆußaK 3Hªù±†?\H•à§š 16+‚À„€çCŸ “ðAçèîk+ KÖ Át±ZȨ&*U²h¼_½îh›w)aŽYŒŸ`ÙÁ÷ A£œ¥iCªPx‚M(£ÇG!”—·td9IIz±:ŽY#4U590 Ø@™â%áw‹;eî|£hî‹j]å^ÕF¶˜'w´š5q KH,6.òÜdD£!-³Bí,'³ r==œE‡ÊlºsEÃù£Fáù¼‹~S‘ífB&þ¹”„ áhGTÓìBP¹ &“FX2`É ýÈ¥%ˆÑ à0ˆáP )ÁÑn©¿ŸÐ ˆÐs0*­8l©ÚÌ%€¸´;RüeJ‘YG‹ÊªRí¨Ã•´ÖlE=%SBðSçÄ ¥OsXÌè£@X²…™*-à€¥ö©·!Âò²UÉæn¬9TŽ8MF7<­¬ŸX0@.Cˆ `rᆂ@ ÅŸ˜ç•Ýr—¦3Ô>æ«]”2M`ªZ©Ñ k`óX†¡F ]€„$ [Õ‰ YB«D{,EÝg=-ã¬gA»[À&ÎX¥%PF°þ³íH7°ZðÀ[¶Ò÷!ÐÊŽ¦Z×ѹC N$€ö>¯ŠË8z„Ë¡NIøkÜê?Y¯ 0—°É !k*˜p$ß[Ñ68ÞŠ–”Õ‚*ÏnËèb—±î+vú¬„à5¾is!¬ ¬$±%&ŠKƒUô×±b I Š*ôY/u×ÅèµB§% u)4À6à£y¯$ÖCe1f.”y SŠò&X a9î5AiSæŒèm{G#§šá°€s¡ÏDHYì>’”DJžÊKO÷Ü%ó0‹3T–C‚aŽtùD(Â)£Ýð­þ8ãu¨Ôó¦G ìQ•¾AލIÍj•`GÓ•‰´/‰@‚ |¸Îµ®wÍë^ûú×À¶°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØfö6 0i ‹FôÍ£(šûÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~ûû߸º Ô‚l{C€s €# gÙŽ¸Ä'ÞkPüâǶÅ3ÎñŽ;{ã¹È‡Ý²/?#e ¸õní;Ø-|ËÈ‘É7æéj,z7—FΡAsjÄÌuÍK |;|„žccçÐ@ú3”Þ £¿ÃéÖ`z3¤Î ªþ+êíÀ:5¬® ®'ÃëÇÐú:Ä. ‚SÃìÓ@{4Èž¶·úípû9ä÷ºïîåÀ»Ý÷^½Ãï|ü þði0ü7øÆñÝ€¼ã'ßÉoÃò”Ïü0Ÿ ÎkþóQðü5DúÒ/ôÕ@½éW_ÕOÃõ¬g=ì×€šÇþö›¯}3”|Á3ÿêÆo¾ô™ðüdTúp¿~Ø£ýî·žûY¿÷½¯}c”üŒ>?1Ôþ(³_ïo¿Jã úËÿ‡ö÷Eþï¯ýóÂÿüG¨ €8Q€¸€€X hþ ¸€2ñ€´ ø( X,‘°À˜è !ø&1‚¬`‚$8(¨ +˜‚ Ñ‚¨ƒ.Ø2h 58ƒqƒ¤ ƒ8x<( ?؃„ °1×lg&„kC„Ÿ°1÷f{JØMLè 3p…X˜…Z¸…@â…‘4…°1`fx†h˜†jhð…`h=bÈ d¸†t¸†m…o¸„nØvP†uø‡fx‡yXq¸ sˆ(ˆƒ(…{ˆ‡ˆˆt¨ˆ‹ˆ8w}‰u(‰“¨‡xȇ~ˆ‰j¨‰›X&…¨ Šh(Š£(€•h§ˆŠØŠ«8¥˜ ¯þ‹ª8‹(‹äp‹¨˜‹ºhµˆ ¾ŠÀŒ41Œ—PŒ˜xŒÈ¼øw—‹©ϸÊh ̉Îxh…7Ô‹èßXŽ–ø‰ãØæ¸àÛˆˆìØŽ*‘•€8ôˆöH ø˜ˆï¸"Ñ“ð™ù‚y⸎ ™4ø—×Ô¨©‚© ‰y‘A’°‘vØ‘i  "Š$Y’C¸’Ö’ih‘,é' 0Yè8“!Q“p“g(“:¹<é>IŽAY.™z‰‹Iy” 1”P”lØ”N©PÉR ”Ui’T) YÙ•þ[iW¹_™“a™c©ey–$‘–‰°–l9`ÉsKù‹s—á–ˆ—xùzy|Ù—i–½X—Æx—‚Éi™˜9ˆ˜Å§Ž ™Ž‰‹YY™>H™Ë™šY—Ižù™šƒ0š¤¦)¨™š±šqg˜ÍÈ™® ° ­Y›OI›Ö'›ÜÈ›ºÉ·ù¹œ 1œ~PœÆyÈÙʹœÑœ|ðœÐ9Òyw¾)ÀY®¸æ—ùèÜ)„)’É”å9žÿpz@êéì™î)%mIøžð'žëž™žRà„öæŸøùú9 óz}Èþ… Ê…^( J€j %•’JHÐ2üÒm Sãø“ú †¢¤ ¨’8àF~¢ pk¶·¡OСj”"Ú›Z ”¢Cà'qJ0ê2:£ZY£·Ÿ_@cd-SŽ?ÚAú¡CJ¤µ`¤^€¤§¢Ž‘ àRBФÔ*¤$*¥÷8¦¥€¤:*2Ð'Â¥^zz`:£Sy£dŠfJ hê"<§Jð¤i§wÚy: {êŸõ}çi—„ú¥a ¥‡Z¨D9©¡¨M°¨D°1žšÚŸb ¨“ù¨”z ëĪC× dWº"~ʨtþ¥@:§bjª§Ê‚–ê À~ÒÀˆsC €àð¦²z«£j«’Š«¹ƒ»Ú †$Âô* q,2 ‚büyЊ¤ŠžÑZÓZ¤ßÊ‘áZtÎ:¨åú Tj€šãê¨ïªçê€é:’ëê­‘ê®÷J ýJ ó*®íZª+°»·¯*9°›z°äš°ê°^ɰ19—õz˜+±¦˜¯Sj±8Ù¬ÿа›€; ¢ Uk¯%‹®+ |)K£`±³³/ë'; Ë®#±9‹²8™³Š±-«±Aû±C»|3[§"[´K›´•µÐª7û´Ì*µxJµþ½iµ¿¹²6ûµZ»µ ëµÚ ¶G{µck¨\‹ =ë¯P»¶l[¶q‹i+¶r+‚;‹§fžhû³.›·'¸·±ð¶ß¸H+¸ƒÛ¶Û×·¢j·ˆ«¶Š««ŒûŽ ®Xû¬“» ñÚwMK«Nz·g»¹”K·Y[³¢ë·¤› K†û°‘‹·«kƒ„k¨—«®™ °³[ ­;¯«1©û¸»»ƒµû ¿Û¥Á‹¹ÃK¼•[ Ç˲±;ºË; ½+϶Ò;½¡P½qp½É‹»Ú»½Å«··Ë¯¹K²áÛ„ãÛ Þ½ª›¾êÛ¼ûY¾ {¾@ ¿r¸¾ƒK¿û·u‹¿ù+¿ú¹Fëþ¾Â À+ÀùÉ¿! ¹ÿ‹À¶¨¿œKÀþ{ºLŒ¬« ¬²ö¸¼Œ̺ÜÁ‰ûÁ ¬ÀÁоlÂ:‹ÂæºÁ4ûØû¾,\¦. ¯#ìÀ\Ã!ÂÒ ÃN«ÃšËÃ-lºCŒº¬¼DÜÃ7Œ¯@ ººÂK<µF¬»2ü½æ;Å(éç Â;¬ÅXÉÅ´ûÄ,Å`¬–bÌ»9ŒÄf|Æ{™ÆÄKÆ|ÄnL–pL½k|ÅI ¾ulÇMÜ ^LÇ}üÆL rLÂ’;È„\Åè«Çm¬È¬yÇ@˜Ç^0à ɘ)Éâ{ÈBlŘ,šš\„”\yX\¿ŸÌ˜¡¬¾œÌÆ_|ʸþ™ÊT8Ê\`ÉJìÊ‘\ȻȞlËÄ Ëc(Ë[@Ë|Ì˯ŒËºÊŽÜÊČƜ ºÜÈË윾œ¿È\ɥܿÑ\ÌŒ|¿Ö¼ÇYœÍÒÜÌ&[ͤìͦ ÎÓ9Í#Jγ|Í ŒÎ̼ÍÜÍ Ï¾«ÎËÎÁìÎlÏí‰Ï¬ÏZ ÌßìÏwÀ½pðÌÜlЮ Ð,ÐY@ÐçÌО+Îè ÑX ÑØLÑòêÐ ŒÑ¹gÎÍÑ mÑú ÒV ÑïLÒÖëÑÚÌÍÏ1ÌÒ íÒeŠÒU ÒýLÓÝkÓ:‹ÓT Ó3ÍÓm€ÐƒÓ-ÓALÔGmÒ ÔɧÔPÌÔIíÔ( Õþÿ)Õ>¿Fí =ÏT­]]ÔHÑZmÕà<ÖlðÕ%Ök Öo]Ö!]Ïnýx[ÝÃX "½ÒuÝw½ÅyÍ¡gÝ×qÖ|K×s­Ì„ݬ†]¸rÒƒ½Ø~ÝØ¶‹Ø½×;-Ùe×býØ9Ùš½Ùm“žÔ ÚvMÙÆ[ÚQÙCÚW¬ÚäkÙŸíÚK Ûv+ÛìËÚYmÛSÛrªÛ|PÐ °PVÍØ1zÚÀmÍ­$€ {jdÀÖ‰üܤÝy`"î£{àgÙÍÛzMÛÜÕÞÐFºçÜdÚ-»éÝÎë}W–Bº×ñ-ÚÌ]«¾=þÚÚ[½èGîsäm×ÿº~ߊ\½ @!kv»àQ¬ØõýÙ^%3Õ]Ý`Ù.§ ÞáÝ- à Ðj—áèÝÚ3®â_úáj|âêÜ6Ù8Ç5~ÞÞã‚ýãrðIî×:N¯«ån¼_nðýVš —e ·ç~à‹þçj]FèIºF~FnÀËçp;ä¾ãî €Ÿ¾PÝ1^…ŽýE’þ¦èš~ÙîæÕ ꟎ &§˜jjÇê‚\ίÞåœ ¹~F¼>N¾¾ËíÜæ›Ž ^ÅŽ ²v+ó½Í^ä½°rÚþ íÝ.éf.n(ãP,Û©ŸJ¿¿íäÌ~íIª«*àZ n„¯úfg ·¬¿¾ìÁåëÏíÜðßÞI¾Š5Á:¬I¥e\!_Ví4Ìîµúì‚P­a!L‰#tñ…"©ñ—,ñÁ-ÏmMÏ­òÈKñl@ÐòY^ò¦½î&ïä(¯# Yš'¶Üý®î?ßãÕëäS{Ä=\óû¾Ï4_óìzóH1Æ'þnæó2OãW/ñÕß1kƒ~_ËNo°P³Q'£½öæ½åAoãCO€©Îö˜~¸o¯âÕ ³¡Ta±ƒfÍy»{ßáÕ TÝæß‡oøYÏîÕ»@°øxßøÀÛôcÿ°e@\P<òÝöÍù™ò‡æ Ü2þønïúÍÞÕm2YLnùÈKú¥?Ö"Õ]û°Ïè¿ïèðîÄ-5×2i¨[ø—oûÏ]½êE÷IÊÊûÌÜ‘/ô•ü¾ô¥Ï²›o~ÔÕ½×ÛÃîûàŸÔ¸ŸùëßýíoÖï?öñúç/Ô·þš/þòÛ ì@`‰Ec2-™Mç•N©Uë›Õn¹]ïÉeó^‚’j÷_(FûÝ‚TªÙüHOn°Ðð1Qq‘±1ŠmÏQrò‹­ðOÀO“2Tt”´Ôô 2•uÒÒó³m³öH¶7Ww—·×tÕ7xó²¶­¯Ø6R¸Ùù:ºXº:ëU™èø,9;ðÖ:\|œ¼|ŒÚÜÛ;œ›“]hÛ¬Äþ?_?¿$Ýÿ`À-èV[çm^™nì’abD‰)JdVcFánœF'^»‹f"tG’N•+Y¶tYÂI3iÖ<ÕÑæ®þƒÙžƒ¯§˜’<¿I4B,adÓ7E{lÒRhȬÐQy}‚Ï#?ÕPÒ@3mÐ5OufS;‰Òà¶ÃtU*B¥²VaZ½sÃ6¯œ1V÷r­/Õ>!Ö‘] |ITKUµÃbE=öÉ[ϤÙE”ÙQ›°6Ol÷”þ×lA%÷ZsyÙ¶Ñ^+öYcƒP]R­·6oÏëVÜ?ãvÞ%ñeÜGÆ…]úø 8ËY­Ä·`Oû=8Ž„ç[Øa_ßù·Ü#†•â¦ô½Ð]Yá½÷d þ¸Ù«›Xá’…M™Ö_UöÝp]FÅbù0¦ùa”8N—b–GÞ¹<˜/–™^ 5eœ‡ÖéRzÞ—i~׿š!ÎÙàªI¹šäì¸Þ8j´oþzê°Å…ìñ~î:h…À–8d£•~Û§½±6ÛdºŸ¶»m¼‹¾d¾)‰[¼¹ÕöêµëÕ[ñP2놷üñºÍ¥¼rWŽn<óI3>;rÈÕ=ôdGÇþð™Gð´'G¼åÖQò»lÛk—üwÕ?ÇýuÝQå]îÒMÝ\öξðĬøéOÝóª|‡~ûé ¹œºëƒÏ­ès¯–xä½P}Ø»/ÿý¨ÒÏ{þõßzå¡sìŸÏ–uö3þ ¤?y®ãû²(ÀïU¯€±kÿšç?Úqx4 #¾ò˜ïh ¼Û§Ás•0Ô'è¼ °~&T Od@€!°‚ |!c(ÊÑp8Ô^‡(<’Ї3Daãç$â°…#lbûV+!ˆX4"ŸHÅ7°ƒ6ì˜ øÁ)®J„^|ÇèBͱþp…dÌ!·¨F´°±†n4ó¢¨Ã2ñŒvÌ ƒ(F¢ñ1Žü#)HçR8W”_áøF¶5Ò‘èäm$éDLºˆ’—Ìb&]³IÚtr1Ÿ¼ù†•FRzÇ”³A%{T™¤ZºÉ•¯,¥ I5DZR‘sä(ué¢XÊf–¼¹e¤–©¥\ó‘¼Ôãò*¹ÇjRS”u„¦ŽYšd†(”Áì#Y¹ÍãI³™¶ê¡Ç9Lmš3M@Œd/ÝvÍý±3‘«['<£‰Fzî—ÖÄ'0õN~jÄL¥BiÉÐS=ó êì¦h¾éƒšÑ¡™‚hDÇ5ÑýSz¨8óIÎr´þ£ }gCWªÌŒZj£(=¡J˹Ж‚3NŠ©L¿%ON‚ô|ĺhkêÏ¡ò `ÌJ 0€€QØEH ¡iÞ“¤ÅèMaºO¤¦”> @>ðl €@ð{œëª (6³:Ò‚æt›JMJÎÊ ­·SOãzùbÕÂdäKeªW¤<@M˜êž0ÕØT”xõ—fm´Ó¯:ö(8@B€ q8êZ÷­ýu9„ý•Z÷ ×Ç5öºcnn;Þ5(÷¾Ð}ÛÍ6º¿u÷/ê=îegùÙƒzwÂ+—ï‹CüáíM¿ÇðI¼w+Ÿôȳ”êúÖ¡nk>‹8ôšÚ|¼ÕNm`~î¥_ûé×Loެ>á;w}µij{a ¾ëŸtç÷.|G^Þ-™ÿ·ñMüÂã^ä³Þ=ïýÉø4;ò̇¶ö]>òéo¹÷l½ïÏÍýtÞÊ©W¹âŸný<ãÞðÃGÿïÕosØÿ]öS'fÍ_'ì'ºÖvùÚBùì…ùâ¯0þ÷zNÙôܧMÿÄïöþõ­ïóò/Ì€Ïñ<ì ¯û¤ïû- 0û@Pó,°8Dç^®0ö ðpi¯ÞX0úBîûÀ¯ú¼ŽpûT°øˆ0ñ40ù8õöoÏ«Ï\0í0ÐõLð+Pó€pþAG{ ßB ké UO¹í¦ï ·Â ç ¡0àÎð½/‘ÃpüưüŒ0_ä0 mÚÖP+Ú°ù°°ÿâ0 ù3I³‚p ÏïgÏ ËLñM¯ÛÎ9qÁñ70w0 )1q QQêDÑOÓ¯ ³þmpo°+í«0ÉP±þ>âY±õ\1üpP± SÑq[Ñw K /ð‚QWn±±‘중 {q¹ñÅ1‡™Q1“}± B—P#n“çA›qÏqÓñþâQòÌqgÑþÔÙqݱ!áÑ åñMÌ!û­[0"Åp"EO!ÿ‘!Ò!I"‘Ùȱ˜‘)q ;Rt`r$E²Î*Ò/ò‹Q"²êd²$iÙBR'=’'sR ŸÐ'Ã1#á‰%“Â%ß0)×-*£Q(r'M#çñþ"ñ%Ðr#!‘ c%iQ%u©)‘â) q#‡0-7h*ug-¢-r.î,Ëm)»ñ*«’(µ’*ÁRó’/¹2!Å2.Sp1¯°óÎ05²1¯ïð¿2æôq(sp/%³/iÃÍàìÍäŒg*/S3±20²';sðÞ’åM6¥±.â.Dz,å"7r3“‡P íÑ$Ím²%q2%?ó‘Óó3+Y3"S¢þ’0W³7yS59“9©ó:“:‰…8Ò8ÑR9—o#CS4½„4ñ± ýÏ5mÂ+“F:3“·Ó:Ó5dÐÞ¯;Çq<m?ÿä;Ù2<ó#þ1ãé7³P„S鳓ÿSóÔ.Ô3ô5'³ñ²=ùCJBËK¼Ps>ís&«S)?ÔC T/! CTC(Ô6-4Cû“1mÔ1qFoTEei7ûiDÝRCÉ*;49{ôÎv”ú‚”C›TG…Gkp:IFI}IŸÔI‡ô%t¹Ô#Þ³Š3J7KËTK«”L5tâд6sâ6ÏJ‘”<Õ´M#ÀôŽâS÷RÓDësJO”>S”E7M=íGIOÐQt+õBµFåT.½t#ðT~dÔMi4øÌtL‰”RyRAôS—T£4uE‹Ò@CõT3UQµEaþóéØÔN§1NUõHmµK³tLMÑ“tT­´TItQµQ]•@õQ‘ué\ôeSÝÓToUY7NuUR)ïW ÂRa)Q7±DÿÔOåsXûTP§õê˜Õ"µ[ô[Åõ ¯5G«V1sOÛE]Å^ݰVÍUZYU<óµAóÑ^³µM¡UXßWç´S·]‹Õ_kè^¯T^'va‹´aÓ]}æP _Ö9`=õb[õa )bƒu_K6R=Tù5Wç•OÁsN¶«¶9cëu\eÖaóÞFV@iVQÞ´bSÖgÿ•e#0c±Fh+Z_–bC–aa¶]uviš–ÄþžVa¥ÖbA6i¥ti©”`gõmV;ɵgq–döXUè²ÕP±ËÌ6¿6M‹ÖZí–WÉ6ãäVLˆ–kVm‘Öe‹NculŸµ&×nEÖk wR¶BýV:˜Ê© J H²<@óèPÑ6ga!·eÝö\©v)÷8R¬¬úê JÀ`<€ÀsmìqQ·_Ù6YyjwµN¥Ñ<ÏsÒ“=%7ž7,Œ 2 2@d¤lw}wee‡Åb6lC—gµ`{«–t-?óS²$Qu/u>ø« 8(Ë ÀšLw£¶q{ˆº¸×jo–Xµ`Å×þ[Á"%tAÁ!€ ˜u‡ãª7½Lë X« ø*w½ tõÏv¸twkõ–ƒýWt¶fB¸ w>t‹·|ë šà Èæà‚ËÕƒ?ÖJL¸nM7^wU‡½·{]‡’~XˆQX>¦k…¯‹ JÀ…aX†ïo¯÷ˆØ†±—Z}X=OQ£†Š¥ÒˆÏC´Hk½ž`:l (Ø bX(€ÛØßŽ)`Î <`îóX÷=¦jâXãxŽ7 Žù‘ÙïÙ‘Û¸ñ™’õx‘í¡‘Ù‘#ù츒?ù’A “5Y9Ù<ù“)9”AþàR¹’Cy”IŽMPÙ•ùx•mù–-ù e™éø&y—q9t¹˜ï–}˜ƒÙ;9™÷8—£™—ù}çú‹ Ü~›€æ— ê7ɘ`ªÊלÏÓY×™ÛÙßžãYžç™žëÙžïŸóYŸ÷™Ãw<œÃ4Ì ¤~©÷ H`ÜÊxš¡Ú¡¢#Z¢'š¢+Ú¢/£3Z£7š£;Ú£?¤ú6 ªæ#Å<À¬XŒ¿’Lv  vo·›{P¦gÚ ‚lÈ ÈŽlœIÀ†¬si¨ƒZ¨‡š¨‹Ú¨:4‚ìrgx<À·úW8D@À¬.  ²«s9`vKú®óz¯›€¿ÚÊŽk8ÈŠw— ´þ:°µ MzÏC®™š ü6²›½›À»e<4@Ü À¦ãXX²¹[ÉÞÛIr{ ¸™@ø Çg#»—» äµ< x\:~\µ—@¸1Ì–¼@.û¯äÂ3œ¿¯\´IÛ´£üÊC\ÈoãÇ«Ú Ê¼¶À͟Ľ4¥Ÿø<@8€¸ò{¿¥£8@­€8 üº: ²‰#ÐÕª ½F ±= ¼{ˉ£~ÑÚ­>~]Ñ@Åm„©† så þʪs¡("ÉB`­³Z:Z]"`H œ @¸2 "&ÖÛ©‹ÝØÙ“]Ù—Ù›ÝÙŸÚ£]Ú§Ú«ÝÚ¯Û³]Û·Û»ÝÛ¿ý 6€É  ¶ÛŸ£à:ÜÛý·€Üj`¯B½…ÀÝó}:þÛ×› ßàßûÊõàoì"bJœ ÎZÑŸ*|?œLÔjœ9 â«;XÖ9×À>²`η.  ¶[– ÄC€ÁàJz"(¼àk4D@z1BàÇ™ Àj·Üs·´ ªþÊ~K ~ Ñ#þ¸X»þ ÜsÅj2À`œ¹·{@ÆD ·XìÇ àf,ð]Eà=±m^í#c³YÞÜ—À©XZ˜›Á;¿_¬´™¶Ÿlµaw &ŒÝ—€¿¢l»‰ÝÝ^‰³™ Ê~í2ÚÞz~ Àÿžº$˜ JüŤwúA´û^Î"Þ‰Ñj»o»¶a÷ÇÛZzKšñßõûâàÂ~òÌ~ ¤÷Åê7ØÇ;~ ŒI¾¯×Êí›â±\÷_Ÿù¹ò%ßκ €æ¥Ÿ Rߤ·ôAð[ûï½_®ÑʺŠìù#¼õ›_ý³òi?úmŸæëž îÞÆ {ú þð[Ì~íÿôWLÉdÜFFAÃè|B£Ò)µj½b³Ú-·ëý‚Ãâ1¹l>£Óê5»í~õŸÇx8 Œ'a pt0 $a *t(Ñ¢F"Mªt)Ó¦NŸB*u*ÕªV¯bͪu+×®^¿‚ +v,Ù²fÏ¢M«v-Û¶nßÂ+w.ݺvïâÍ«w/ß¾~ÿ,x0á†#N¬x1ãÆŽCÖ!ùd, X… 1"""+++444<<<MiCCCLLLSSS]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏž;:âÓRÑ4 9ê†)N‹$44(  MF< @C¢2 jk £LŽÅ)¨ 0aDŽ "‰Û2 ,ekÆíß§3à ÂV –ˆYÂÀšPÌX‚ƒ•a×ÓÑš/]:MDˆp6&xõhmDµ˜ÀƒšY+hqÄ=t 2bïí$—3 yýS9ο”(CIw?A„èKr)°;Éw4À£y/9G øO¤DÕíGà§ß 40@@ÀÛf A ñ™à1€Rõ¥ÀP¦þíU!à>‰ÐW05a…D ˜q&rµy$ašÑ@*õhkrü@W.V.ùG¥)Ôº©©ÉÑÒ&C§@¿Å•‹-5…¼L&þ~‰™ŸšÀ<#1‰µŸ§Fu;»˜¢ø¶H!e¨º#LO ÐÌÈñ›gÛ^ .cT„FRš+%È¡"ÀÂyl/¼™ŒÕ"±íÍrdòj!=‹ðð&¡DÂ@#0`.s–$Å»š&@@É%œý™ ]¤ÊáêL ¤vR‘,¤F#¼f±´± nŠ)ö4™Vñ̨ve¹GA‹p)¥ãºž%)˜Ñg³‹õ,h£;¼‰–´DȘˆFþž¶– p@–?$À\Ñ4jŠHþ±gAZ9PÄÜúªOWàOp ¹@àOkÒR„„€ª %ØêäZË2‰à³Vù“a¨×`I`KX…£VZ¸….±;#qMpßý<àÂH*Bùš¾–Al’õàR`ø¯“Ä@– @‚3‘è³x‘ÓW|^*4ðÀŒýU `€šXE¥­òá¯3ÈÔä7\²Ÿ:‚bÚk‚ѦdÁ!”” €4•9 xèõlü¤Ø#IùHÂ䘽ìôÎf@0™êù+$^ÞH–t¦#Ÿaóüs³þzÚùÒ ŽIxV„§bŸµªW¾$  Ñà Ë>‚ZÛúָε®wÍë^ûú×À¶°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj'û g4j 9#䙓Ç|,"ÇMîr›ûÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~û»Ü“…FMq¹i5 ½UÞ-eçií†;üáºFÄ'NñjK¼âÏø²/®ñŽ{سÜ24ðÃZ€Ö¿›Ÿ ­¼IäP=x6] šSÃæÒ̧q™'þî‚ﯿt®éškçÒ@:4ˆ¦cCéЀú3¤Þ §þ¿ÃêÖ z3´Î ®+ëí;5>vm=ç;¿zÚWÍöCˆ}oo»Üÿ÷tÔ}îxÏÃÝϱ÷¼û}}/GàÿNø6 ~‡/¼âÏøp4~ñÃã¿1ùÈ[ž@k{æ/Ïù2T¾Ÿï¼è£úm”~ô¨_Âé³±úÔ»¾Ä›‡{ì_Oû)´þ·¯}ês_ Þë^ô¾ŸFðùáGÃøÄ‡<òŸ±üä¾ùUŸ½ó§Oè3ÃúÔŸ;ö¿.ýìOûÉ¿÷U-þc”ü—>1Ôþ2³ïo`ã úËŸ’öÿEþï?·ý÷Âÿüg¸ €OQ€¹€€h xþ ¸€3ñ€µ 8 X-‘±À¨ø !ø'1‚­`‚$H(¸ +˜‚!Ñ‚©ƒ.è2x 58ƒqƒ¥ ƒ8ˆ<8 ?؃„¡@„B(Fø Ix„±„à„LÈP¸ S… Q…™€…VhZx ]¸…ñ…• †`d8 gX†ý†‘À†j¨nøqø†÷0‡`‡tHx¸{˜‡M×}|ˆ~j}˜…8ˆšWtìpˆˆ({Š({°lbÖˆ1Áˆ† sôöˆ”‚(x0Š¢8ФXŠ@¸‰,a‰… s`°‹²8‹´hþŠª‚¸(®X‹¾X‹·¨‰¹ø¬H½ø‹È‹Á8Œ ¸‹Ž¯˜ŒÈ¸ŒÌHÅ8Ç(¾HÕ(×(Ù¨´ÈÝhÎá(޲HŽåèß騎ÊxŽíøïñ(ìX+qtò8ŽôÈá~급Ù)Ž ¹&Ñ|ðÚ‘©‚ z븑i¹)’/’¦×‘9ˆ’ 1’zP’Éx’0Iƒ*© 499y“Mؓذ“¿h“>™ƒ@‰{,Ù’DY”!“z—”¹”LyN‰B¹G9•Q•wp•À˜•þZÉ…`I ^)/–T9–•)•h‰„j‰vÑØ’¶—où\ie9‹ny—?y–æ°— ˜~yY‚‹}Y˜Rh—KÇ–ú蘌™‡I‰é’“©• x™’™™ö°™rp™uI˜ ¹¢¤¹˜§)–¦‰x ‘ŸÙšòšp°š³I›ð`›o€›¯©›Á›nà›À9Âix±y‘¹Yœ‹¸œÜ7—JéœÌiwÒ‰ Ä9ù‡Ð•Õ‰žø›Ï¸mÙÞ ›à‰ŽÉi’äYžÏxž”—ž5¹žìùžîé ×9ŸW(Ÿ§PP-Wdâ'ätŸøyÇ “þxCpDð#´¦ˆZ Á©Ÿ©Pÿ©A/9¡†Š $¡D0K Ÿ;†@û³äЦ«²E›´G+KBk{K³,µ­µþ1¨³R{µ=«µ˵¨PµR@´Ñ™µb Ždkƒ^Û´X»¶;ضc»´6º³`û´j+·ÿ¸·Ë`¶¤—·iË·@H·sk·£úµN;¸„[„†K € hË~Û¸3ù¸…‹¸´ª¸qk¹Jˆ¹¢¹O0¹ãY¹žÛ• ë¸š ¬p¶§Ë S;¢‹²‚K¹¯û„© ³Û¤‹¥· »¹û¹« ­x»¸¶û»š»_;¼ìÚºz‹¼É¼ž°»LлžiºÐ{›Ò‹»Ì˯ÅÛ¹Ùk ÊÛ±Ýk°_`½œ¾^¸½À[¾{°µ[ºê+¾ì;²îk´Õ¿¾;¿”0¾ð{¿2«¿×Ë¿ý[þ¿Ñ Àßëº̰œ…o ¿Æ+¿ ܆ Œ Ô«zœ¾ÌÀØk~|¾|­ÌÁôú¼Î˸#|‡¼¾̹ œÂŠà¿ ÜÂ'|¼0ì²l ¬è+Â7ŒÃ%ŒÂ ÁûûƸÂôKà ¾FìvHŒ´J<ÄLÜÄ[›Ãë÷Á^ÐÃÞJň ÃYŒÅ] ÅåÊÅNlÅİÃI ÆLKÆÆøÄÅ_LÄÌÆmlÆðÆ\ ÆwKÇuÄ6,Å/ÌÇéÆhˆÇ˜'Ç,ÈøHÈ’€ÆH Ç‰«È‹lÇÂàÈGÉ›+É}ëǼĬÉOIÉõgÈZ€É¬ ÊÉÈ ÇaÂ[þŒÊ©,ÊÀ`ÉF`ÊÄ Ë±ÌÉE È&ŒË¡¬Ësìɽì˨+ËúGÊY`ËÍKÌŠ̉ÌËBÌÌ–©Ê ÌÊyìÊc,ÍzIÍrˆÌX ÌÞ«ÍÛl̾@˰‡È>,ΜIÎÿçÍWÎæ«ÎÚËμ`ÎaŠÎ¯,ϪÉÍëÎVÏï«Ï½ÉÏ*l͇<ÅÍ^ÜÊ]ÊØ¼Æ œôL€þ ±½Ç=œÍö¬4É­Ðí² ÌÉ!½ }Í%ýÍ'}Ê)}³ª=´/}Ë1­+y-ýÎ7½Ì9M°3-«=ýÏ?ÎA­¯C«5mµøœÍIí°#ÃM}¶GÏþQýµKí«EmÑO ÑY­ÕΜÎÐüÇaݱ[M¬U¸_Ñg-Ö¼ÚÕ6ÝÖ ýÖð›ÖÎ*×NÐv}ÍxMàÒ Ð©ËË×ýÉ}íÐ-$-”?ÐiT»Ö’{Õ½Ø~ 6?2§|VÙzmÕtÒš½Ù¿ f!sNR=ÚlاmÔÙbC2×®­±°}Ù¥ Ó³mÒµ ñÂKðs¡-»–=º˜¿Á {Ãý€¡! `¢-Û&ýÛ8ýÜ {e9äíu&ÝÛÌíÝ@ Þ´- à ÐgwØŠíÐêÔìíÓÑ ³èM»÷ÕùmþÕûí M£ÿÙ.àÀá"IAbäËÝ.}àÎߢù€áðY€RÊÝß¼ÛÜSµJhÂüéŸ.zð•aµ¼Üþ-á ŽÁ>ܽa:²â[Ãrp$ÁRà1Gá#~´+ÚÚOÀ£I°f=®p âùKä5ÎÆZ €ãî%¢Cðm4SCBÐÎÞ¢¹°fî)$åJޝFަPŽÁR>ã=åzPæç 0§^ê+6SRvöå"nç\¼Ó&ðç¥vjiN£qÎÃ…Nç6mè è­kBès.é Î '÷éà''ê}žå<M|ægaþ:¦eººaþÈ‘ÎéL°¦mú¦»pnIrªL;—pŠ:Ìq,ã²ݽàÆêÈnì¢Þ|ê§€ª8ÐAUve?"§éÂ>ìMé©xíÏcGBtg¢/Ø^ßÚþâÜƱžîù»îv@P[€áÇñáÙ>×û®í¢9\£–áÿÛï¤mð².š`>WB:£úŽîŽðœ.šp;gúÚî°¾éðNãÀ`íp”#HN°Éïþñî.ït0·Q?£ÎóÚŸÞ5Oç _!€°h.ñ>íñ*ë,x·AgÑòg }ó!NôE¯îÀàUþ% mÛ-ôF õQ¦G/ ”VOóXïÕNŸà¢™µ(Æ#öÀÞÊZ¿õ4Úõsƒ¶`ØAßö×üöp/œçs$LVðcÏïƒïïE¾"”±ñe/çoᇟ1ˆòh‚¯÷‰ù[/šéûÅ'ÿâ|¿ù‘O–oòïî¥õ¢YÝ=úçÜúàèçmû(ÿúEû.Mû÷¬ûÁíû> üÍû*OüFmü.ü³­üÍüCîü§ ý6-ý`~ú"kýV‹ý)÷Û>Öù\Ö þá×Ôüé¿ØÜoÕÞüßþûþëß×òÙô_øÃ~ÿÌþÿšû@™ˆEã™T.™Mç•N©H€bÑn¹]¯¥"¬F¯•ïùŽÙm÷—ÏéuûŸ×ïùÒëº/Pppê* QK-¯,1q‘0Rr’²Òò3SÓéoÓó“ Ëò®qô¬t•µÕõ66³S¶¶Ò5U ï4·KÕ6Xx˜¸Ø8’öXy×÷w×ÀÌ™ xù;[{Û5™û»©™ZÚ®wÜ\}½Ý=¼ü]]=žîœ:]~Ÿ¿ß?ÖÛ¿lôòÙc&mÜ}6tø0 ˆÇ:[ŸEƒ9vôø±H@µ*úºø&£É#Y¶t9oåKP%st“²fLþ™;yö„%Ò§'š¨l¶ÁITgP¥K™ÚÔÒÐQEÙšjV­[Ç<åJHª#ª]&ƒõkZµj½®åöZ²ÓŽu{o϶y£ª+·ŠU±€ù6Ìqïa9pI.Tö¯DÅ“)#v\Ù(³v©Ž+shÑòî¬92#Èõ@›vý[iØO£áü˜.ëÙ»y“Ý[Im]­¯.Hxrå³.+ž¦9Ï‘/·~ÝitàϽÜΜû8vñã³W'O„û3ó7k\~üÇïǧ¯¦S{•ôå÷÷o?ÞìS(@&¦³­Àÿ´î7ì$‡¿ÀôË)B*JÃþ 5Üà KXD¥¼îÁ³*ÄÍ,U€Å]|FO ‘F†Fd5Ýx™©C¡ ‚ …’È"+  Á•\çÆåJô.ðÜSMÊý–¼¤&Ë1¼«¤Ê¡Ä’Lp´LîÉ$“8p¸07S³Ì8}ƒÓ´4}”ŽÇ«î¤-ÏÁö”PŠèÍN7SóRÌA]´EC+Ñ7ÿ„ç˽´3·ãrÊH¦R=1ÕLG1ƒÔD'5°ÏÏH}UM{CÕœV©3TGXutÕÙh­ƒMèz ÎV‡ÝYIdS+=͵ÖPýL–Ú`–Ýí×{ŠmóÙ.«ýö•k}mþÌn;MURpÕmE\ز=HZWÍuvÝz7i÷µw‹‹÷Öy˵àKðuMß8‚íÎÔƒÕ ˜á[L­¬`Œ¶Wo¾Xë$×R;F÷S9/ìä’Aøã=4&”cQ=vdhã¼"Æšm~8eV-b”&FøXù5vQCŒ<ÚH$ƒÎÀ¥YöKfm‡æ6f‹gþya¦wtšç–§}ùëªÏ•Sáû¸ÖZgœ'뙽©)[^@Ë&ðl­w~Ô븣Uµâ±¯vh´#¢‘m£°6»ozÉ>œnÁk­â¼û[q`‡°èËUt\[È)+¼*ÍÇäð¬å]mŒï>Ur¢ážþ|ïtsÎò|íÖ©Ž=dËKG|vÔqW=õÃ@Ÿ‹ï×]?ý÷Æm/nxãu›òyšLÚ7^âè ;^Âæk¯þãÅÅßž{ö¼/ üÓÎ=¿÷‹ÏWøô»'|úÀ•ßÝ÷ä™÷ß~¡[_Ú‡"Ù‘fæ`%4À¼ð; ”š³W? P³Úx½ VP~´×ê"§A¿q°w(Ä–´¿’ðsù3O/ ²Ð‚l`oh°Ò°rejáüÃÜ™0ˆ6ì`ÿ–(Äê03"¼ £ÔC‰…0sX„âi¤è*âI‹ ››¸Åütq-_$}F2¦þÐŒð@#[d<1ê‰pÄ¡ãxF–q_ll› ñÇÇ>²jŽiQcü:C‚°‘‡4¢LJÀPÂ`D‚#,’R‘ Ÿ(4ÄDVk’/Y€4ð`IÐ4‚`@H´^WèÃGî‘”fLeK:Ë",`²Œ (YõK_Ó?¦<$žNéÀÀÀÉê0s ÒÜõÞØËia˜,9ÀŒd ³ЀäšUä3÷¹$qž‘®)Œ0` @>àžè¬£óî;^~˜ýhÚªÙ´E(D€|rþ¢ãâ.UˆÑ+ýs£k"âHÀÎ"¸SN`6%ªËò]´†Ôh`S”v”!€¼©Sž†HÀTàSþµ4¨üÔãKŸ¸Ñ 0¬ @ÀLARLM™N(éI{šO0ºuC-$\˜N–¬RPj,MðÊ ðQ¨ZuŒ@M¢P·êÏ®=dI%/™É"ø•0@0ˆŸru$]C)Z²‘EjVÀI,g^‘´äAmjíúÀ”¢O±,Ubc•4[ÈÖ/­•ékÛÛú<ö·’âm]ØLãºö¹B®Q»\Äâ«‹Õª9{;ݘV׋þÌM˜w‹kÚù–ºÊ ïu-š]Ý2–»D%/@Á›FñÖ ½°5o)ç{ÎúÒ‘½Ë[éOÝK`HîW˜êµo€¯ºÛøޏò° àè íÂ(pˆò»Ñÿ*ò¾‰{ïv]êØ K²Â f09sk`ø–¸»'îㇿ"\ #8‚8îO‡é›â‡xœú«¨€ÿ–aÇј+6Ö°‘¡*cþ:9Á«…Š’% åóVÂLF’·Bå&k9mVÞq§Ée­x9²b~«ŽåÃc K¹)h3˜Ý Ž‘™ ÏB@ÈŒb83EÎuF›—A³›ºE†.¡ëêã$yÈ ~qV3…¤]ZHJþ#IŸgìè.CzÀ l±¨÷!hL˜:Žff-¨GØ“š4œ˜¬£üG,C˜Î´ˆ®)êZs˜Õ¯vµƒaÌ$^OÂ×[Tõ”ƒMlJ—÷Ö¥R³²Ž}Áeǹ֮ٓ~£ýd÷ºÚV´ˆ²Mâg’Ñ‚š62ÂýBOŸ¹ÜÜ~ðhÓmŒo#»Ýö»6 ã íyºÛܸ7µ×íîq%Ðù§Â‰1pv\ßï^õŠ…êak{Ä•†xÆþ¼}/%á_tÀ·áp°tÜv'7ÅgØjì:{Ûì0yvDîq‰3›åv´øË1îbwÌœã5OùͱsŠî¼½0ϸÌQ®²¦™èü6þºJ]žôžÃú@„Öm~pŸ„<×B·ÖÓEìóïFäýF7É Èö]‹]\º×õ¢v\×{ápß´ÞƒÜòÔrÔÖÿ%ÞG.ø’“½ï:ÿ;à=uÜ ›ç掹±ùiÿ]å·;½Ý®ÏÎ_CîN¯<Ó2ÿõÍÜðmOýÛÁŒt"cí+ûì ?ŒÐÇñv‹½æßܼÓþó…νë%^ºó쵿qð•qû²cÝø/öÝ™_ÚêÛ{øU=mwoúÞ·òΗ…ø-_ñÅ—¾îßÇoîÉÿ“ìCÞêÑo<ðWïùúƒþýJ7»‡»Ÿ~úOŸóîOøFOûŠïìŽo'’ïþú~OùÆŽáoû’ eB‘ÐÚ¯/û&ð%*Q/.ÿ®îâäï´Noð°ðFððPÿ ï¥ïܨÏ­ï±M0òfpþ0ðË€0ÍZ/\JPò–®Ç<Ð%@°ÐcÅåí •­ÿOÝçÿšÐÛ¢ÐߦŠÐÏ ÕïùNP µwð÷/ ipòlР׌m ×.Ó ð Uï ã*…A!ÐùO [‚ ÛÐ Íð‰9PÙ Ýõ0YOËÏï:ÐÕ Q'16æp±ÌªÅ0ñŽnͯÔ@ñ9±Ó‘% Q;þѱ‰Qð¼òðÊê°=ÿа kñ×RÐ-QñQcM»­)ío/°Pc 1eЉ±«Qݲ‘ãÏ1 Á‘Ÿ±ùX‘gñ»qéPë‹°é‘ N{ðá ï±Qò15qaq$dÑ ßq!«àÎò Ïö —‘þx2K¦}q¸r Ñ>rý²r2"KF"‹#?B!½QÕO€Ó.MÓN‘ê’Éí#mïPq"/±#0 ]² R,2ídr&¦&ùl%=¢%çñ(©r$2 ¯÷DRþ¹r‡~P+ƒ0,‡°ÉO)‹Ò*YÇ+¹¨Íñ±Ra’9rùèRôrRóÖr>Ú’(‡‘$Ãqíq,çL µÑYF/£ˆ/ñ%ÿråÒí²Êðò-‘R-)S}3ý.2q°p0ƒ2Ç%1C!*;b*³*Yó*-s%s ]ó2 z4“"Ñ’6c34ñ3S6mòñL3 '5-(SQ(uÓ-³3Ó1ñí43r:ùä8'b5›³5µó5Ó’73芫ÓÀ28 s0Í’ýÜñ;s¹ÈSŽps996³ò<Õ>³7í+?î:!";û²2½Ó>÷³þ. ô.m“Üú“Xþó!”1·S@³>ã2: N<»2C¿Ër=ÃïC7´@Á3¼T¦Ô! t3t7GÔ=}óBND ‘=ÙÒ<t2=DwtF=3FmËD›F>Ks(#´;[ÔBŸs#qt6¹³6GÓrP´!T47é“@“´B}TI1TAó²GQsHQQç“E´=T?I´ÎÒK»Ôg¤ÔF4r.™”,Ó3DÝ”B¯T1ØôMÃô& ÓL]MEó>kKõTz‚ô¨üt8‹tEóIµ4K¡sKeO­PQ% N‚JÉRõP#•R'U:¿”:MÕ:Õ÷`´RASMþôU—ìRT÷ÔPóFc5Gí”GuU#“Ná-S¡jSÿ¡S‰”9'TB”!I•K¡Û†UµTüÒqPYÕYUW›TYñFZo4*é¡,K Hàš<@¦’sL‘ÕJEuI·µNÏ“4Å”8g•,ŠÕðÊ•Œ JÀ`<€@]QŠ]µ[›QãõE;_Ï´UƒëVÁt4Љ¯L ­ v` ´xÕZË'c“•Y%6[ dßµV)Á$O²C$re?O3s4”*›¶ ÀŒà¯ÚÊW‡6d@ÊQ«´Lv ŒÖdõ^v’'€ig¢b©É4dþoŠ– žŒ ˜ö°F6eÓTq¦6iOVP'– ÌöS•vÖ˜²)‹ä)M€mŸö[}ª  Œàà¬Â–h Õê–VÝ6T[6 —aáõY¡ÅqO­jSu4>ª `¤Š üpaŶaçtl&7mÇ–Pƒ•nÛÕ^+ò‡L7**·@ª©º2:€fÉ8€>¤¤ :àš ƒ»g©À»F œ= rÚ¶ ãg}ùn)‹©¹¼Ï>( þ“ð30 é©L¹ F‚˜9K˜Á]$F€  @n{+ FB"˜‡ù“C\ÄGœÄKÜÄOÅS\ÅWœÅ[ÜÅ_Æc\ÆgœÆkÜÆoÇs\Çw< 6`³ €l:Š‘à:€Ç“ܪ€î{íB Û²@É«Ü0Òš 4ÜB¦@ˉಭÌï–Yd»z¹ÒÙJöÒy$`–l˜€³:ødÀÚ¼³”Û¦1à*¼lš¥;Ìz@_Ä­ÃÒµBf @ d» `ÖÜ‚<‹Ù9“:€æé¯´þ ¼é<¢ºÀZ]Wi2À{µmz@°DÀ ø*° à+¨\g˜<œ#ÝØ¡âž‰À̉à¡Y„ݬ±yªÿÊ $Z‹=Ë ý•Æ É‰@©@˦=šÈ•Ý©M …‹ Ø]Ý›"ÙMÓ‰ ²vÛAªk‹ °ÿj–† >¤Ÿ³]"éüscɦ­½¦ýU¶‡y–ô8Ý×á}Ë—Ýô¶àÑž:ö¯d›E,¼E :´k{šo:ÐCþЛãÜË…½áWþ%< Î â{Ù k€ieß—C¼ýã¾9–D*äùJ¶O~­-žå“$Ú=晖≠Úò‰`#¼àáÚú¸ý²"Yè‹ÀÐÉ›9)hë]é˾#>àå‰àÝ#Þé³Ô9@ÔðÕhêWÞP€pÖ.À p5óe`@’Ô Aþ(…r„é4ˆ„À@Ux@„O"4 @0•á†D ˜l9Ðâ‹  GÂ\ ":€žXع·[ñÍ·âN!4€âCl€@U`Þj;RÖRA ÷™ Ÿ ¾8À¯‰Øåß æƒdzX^xn@â›_òG‘@áB,•Ûɶe hæFh [c ,ö‹AA”™áLÞH"Ž!¶Å, Áë}§Ó•8‚Œ€JD™¯ €@• §x€“BT7bdª„lzÉ—Ä©º®ºžžBÄö`h,°%[ èìzTbz-§¸V‚¤þPj)©Ýîê¸Cˆ«)¹7~€©\ zÜzû®²~N$¢H`pÙ•„ç¤&À‘FxpÀ¢h{„N¼–¡d&|@Ú•€ÀÇÖÊ¡¨õ©Õ–NŠfWá±Lã‡&|Ê¡‡fâE¨þ ü‡ÄÈXáÙÎH९¾è»±@µŒ ÌSèhwH³lkÍ á1ÈBˆL2Í"qÉk]2cçèÈ¢–}lÔ[g¨`ÀÅÆö{á•b•n‡]){Itp€vX|Dl} Qét@­×c·½µåþÄT}l °¨gEë4À¢Ly|¹ 6&jÎ,ÀÝF ŒDþlÇçíxë—fŸKnBäE Bå:­nÂùäm£L¦›pâtl¼ò¾ñõg‚h}òx?ä_vOÀÁ„“*À @| (Á%t@™I ?Ó#ÇO–o%¨œÍj·••JsÙB’Ët²7š påâXØp ²;Bu~²4ýÙå=FhÒ³HI(3œ b™"\i:¢ ñ32ý5­iý‘…02!d€ÚÏ“vh‚+…Ï!0Ô”P6ðñlBh€b¸„óÝ/ ¢ÁžÚŸE™à}Ú3Ù­œ‡@Ö¤s3AL…FúÆt'ã^þ‰@Â!\è† ^Ž&ܵÇ ä_vGÄ ñ¤‹.䨈`(!ˆÆ‹"ê´¸ÅE¡‡&xdÀH³NqIGLÈøÑ’%è :`x±™yåPˆÊÊylfB€óF¤2ÂhR³$ÍnY]J²¿<žñ†P"ª`¶%ÂßÔQ’ -oµBO6ÒÛ [iË“áìcó#†R”<%^$K˜p #AÅêiè%xDØžðæœÑŒllï˜Óüæ8O·ÍsÎóž{;hxކ íH[yº“3; ¥ £ ¸®F©©1õiþT=Šú3 ¥Eì½æG7>BÖ³qõh”gwÆØá±ök¤ÝooFÜ—ÑvwÔ]êÚ˜û2ôžŒ»³ÃïÓøx5O ÂKðê@|²/Å£ÃñŒ< oÊKþòv°<94ùÎÃóâ½çG¯уÃô¤O=Pï Ö«þõ^p=7dûÚcöÚÀ½íw/ÝcÃ÷¼>€o â ÿøF0>5”üæ3ÿðPo¾ô}¶WúØ·Âó±~ýì{? Û‡Fø¿¯úñ«½ûäO¿Ìß ö«óî§;úßOÍñ¯ÿâóß÷ûë_ýü‡ øµ6€Æ`€HhH ˜þ€”Õ€ÂJ 8xs¾ ÈÈ ØO‚º@‚"h&ˆ )x‚3±‚¶à‚,0H 3ƒ-Qƒ²€ƒ6¨: =¸ƒ'ñƒ® „@HDÈ GX„!‘„ªÀ„JèNˆ Qø„1…¦`…TˆXH [˜…Ñ…¢†^(b e8†q†ž †hÈlÈ o؆ ‡š@‡rhvˆ yx‡±‡–à‡|€H ƒˆýPˆ’€ˆ†¨Š ¸ˆ÷ðˆŽ ‰H”È—X‰ñ‰ŠÀ‰šhwþ—žø‰ŠgФhj£x«˜Š§¨uø‡Š®˜f­Xµþ8‹›'‹åp‹¸zº˜‹°Ø‹ ø‹ãÀ‹Âø ÆÉxŒ³GŒ¾ŒÌˆSËøÓ¿çŒáPÖX|Øxzݸ7¡“÷àXâ¸çXŽâGŽÝŽêx~ÐøŠïø€ìØŒñ8ˆáŽx øØ÷XyõØ,Á™Ƀ™ ²rèhY… y÷©Y2°‘Ù‘ù‘@Y‘ q‘t `*¹’,Ù’.i0’$9‡2Y (ù’8ù’1I‘3i&97™“B©’;Ù“"ñ“r”C™“Ei” ”q ”K©“5é”}X•Ó •SÙ’Mi••þŸ)¹•TÉ“^ `ùZI–D‰•géiékÉ–]ù–—m0—dY—vyxÉz¹•|Ù—_è–Ü7–lÉ’ƒI˜ñ—k˜S¹˜ŒŽYzb™˜\i˜“i•™¹”’¹™Ñ™pq™˜¹’¡)šn¨™Ïð™C™šª©¤y®)”°›1›fP›LÉš¸ùºY¼‰“·ù›œ«gš§ “¾iœ¥h–¾ˆ˜§YœÎ)È9ÃY–Õ9šÍÉ Ùé’Ô¹ÿpbð™ âÉä橘ݙž»øžÊО¨)ŸðùŒ›¨œÓiŸ÷éèI % !rÿ¦E§DŸmùþŸýÉ™ü¹ " +‘³oÒ¦uÊœ º –Ø €&FG )‹öJž‰Ê $¢F4ñX¢)z¢×¸¡Ä°¢:¡{! QO'˜i¢2Ú¡4: +*¡BàÐ\Á£0:¤AÚëyEêeCDO¥Ö£ªP¥N@[C¦Ÿ?Ê¥Z }P ¨v£H¦D pš> šdZ¦Ë° § C×à¥Lp¥a:§tY§v ÖÐ ) À9þ¶À€M¨Ë™ … —„J ñ;a9vUÁtÕ0bš˜@š©Ï™Ÿ‚º—›ªªþȪ—þª¡°ºSúYZ«ùp«±wªƒz¦ºš¯Ú ¹¬ ¬×ت‚9¬Æ̺ ÅÚ¬óÀ«]­ÒšŸÈz Öz­Àù¬%è«®š­Ü:£²z©©:®þY®Ëy®èÚzÞš ÛÚ®‰÷®*®Ë*®òš•ôz ñš¯ç@­Ô§¬‘¹¯þšƒ[ ýZ°À¨®û‰¯ k¨Ö'°tê°ëKƒö:°[±óy±³°‹Œk°ûš#²§°[²(k ;¦۲вZÀ²2«'ëƒ;±7[Œ9û 6۳ܳëX²¶ù³B»†HÛ A›´úJ´­¹³& µN ‚K‹„R{´T[µþߺµk´½éµ\˯W» M;¶Û­`Kœe‹¶„ض©p¶n+bÛ±k«sK®;«ìš· ·R˜µaë·Ú ¸)+¸l[·„;„†{…ˆ‹·‹û´/‹ª¹ˆ@³Y ·–; ˜{{ ž•»¹¶º£ ¹¢k¤†Ÿ{žbPëæ§;ºŠ+€«ëžDë07»±K›© ¦ b ’ ’"©»»+œ½k†µ[Ÿ· À·É{º{¿ûò¼æ½¢;½Ú·¼˜ÊžÎ ½Æ{¼Ø©½P½Np½âK¾“h¾pè½´ ¾Ø»®î¹Ü[èÛ꛽ã˾`p¿T¿Ã¾üëþ¿˜X¿u¿}«¿L¿ýkÀÕŠÀ™ ÀK°¿ Á(Áz¨ÀšiÁ ‹Á‰ÀS@Áë×À ¬¨Á—@ÂIàÁ0‹Â)üÀÎz· Û¼ó{Â0L"Ü{lÃë›Ã:¬ÂØÃåiÂ/ ă°ÃàGÄòûÃH¬ŒB\ ,Œ.L¹2üÄ<|ŨKìÛČŀ ÄÀËŶ[Ä7|Ä`ìbl½Lü¿FlÅiLQü¶d̼fìÄqŒŽs< S,voü«yÜkœ¾mŒ«®<ŽZü }œ|‡|¯‰¬Ç‹¼…Ü«g Ç‘œƒÌÀuü½n|É€œÉû¸Ç‰XÉ ʈ,Êw°ÉÜÉñþûÉx¬Ê@IÊ‘ÐÈaŠÊ,ËIËŽhÊԇ˫˻<ÉÄêË+ûÈÁ,̳LÌVëÊ <ÀÀ̳ʜ”¼ü¶Ü¦È,ÍÓ–Ì ­Æ\³Ù<µÛ•ÕܾÎÜÁᬵãÌÍ“ʰüÅ뜗åÜ×lÑ,Îñ,ÏÝü­çìÃðœÏ–¹ÏðúÍ™›Îƒ Ð9ÏÜÏwüÏÍ»]¯ íÅüО©Ð‹PÏOwÏêlÑ¥Ñd;Ñï\ѽ›Á"mÈ}Ð%¼ ý‚í¹+¸-mÒ/°1M½MÓ5œ7±)mɱÜÓ†üÓ›ÓÝ;ÓKÔnlÔ‚p. °}Á¥ZÄA}ÊþCÍÔÔçÔ€@¢ñ㻆¼WýËY­ÕàÌÕ$ñ} iÉYÖǬÔ5ŒÖM P4ÑçPšr ÎtÝÅvÁjí "?Ð×¼ûרe<Ø„ øÒGñsp]¾Ž-Óg-Ùø{ÒXpÐ ÌfÕýØ«íÙY Ž¥²Íwž»Ù: ÙvìÚi- à І§ÚÕíÛ Ú—‹ÔŸÛžlÜÝ[ØŒkÛIÝÚÎÍÀÐÍ6’B¾Ò½ÜÔ]ÝŒÜVð`Þ@[FšÞà]ÜâÍÆ×$'L Ý¢F`ag©ð Øîþß~<ß°™ý¯ƒ)Fªtu&%£Ñ¦Êܯ,àKLà€À×å£!IPrr†rö÷ÝÇ-áÏ\ÝY pà¦ßŒ¤iœ†Í >Â;½Ô.ßÀ°°8Ž*ÙÓ6šàlˆáUìÎ3Nã¿pãä¥åtKà§Èâ32'Bãu]äÀkáàäÅTl;nª/ÎÃ"NÞK³ZÞlDÀj.à0æXŽÁitpþ`tsÎäI¦Ipiýæn§rŠÉ#ÃVþZzº§b—á .¢&çá=ªæ`éE~‘Pér~és^éM€¨ŠÊ¨CM&GgÐtþMÒ*-é3°þz;ŸªH&Àß”MSÎæƒnäj‹êlLëµnÝhšðÀ~Þ~넬ë»>Þmn#ð"‹œdÒ_¾ÄÆ~ì-|èSàôu%À¤<ÚÞÿÍÚßNí¦šìuP¹Q}LÞnêBîÔŽ”¨0ÞØÄÎÀÓ.îŽLît0¹Q;£NDÖõÍOâÖ.üe€::ìî>Ý¿ëH ¹¡UÙÒi½íÀ{ïøŽÍú>O…oÂÍîÄmòÿt!/”$×_Ÿòã p=ÐóÕ>ó)–!j‹!UíÝ2éÒjnÌñ¹îõ’”ð/f»‹öØìó”šÝNïS¿æpo×HIÚj©ÜTŸ÷VÎÊãmöž÷hMø%løöŽøZ­øÕÎø/ø“~ðøzOù©nùP,ùã-÷øùHïù2ïøL-ú~Lú=oúDúެúHúâîú¥.èíÎõsÏùaŒù‘®ùNûiîû‡/üñ üîøCžÊKÏôí¬ü§NüâmüýŸÏú=-ý^ŽüIÙ˯òÍŸËŽÆÝý³ný5Mþ¼/íæßÒèû~,ûï®ûrìþŽ ÿÇÞþÚoÿ/ÿjœþþ¿þ@`‰Eã™T.™Mç•N©Uë›Õn¹]¯}Éeó™¦XØm÷n©ˆÍáJŸÑ}ÿ0Pp°Ððð+Œ‘±‘K-/²m¯àN2’Òq“³Óó4T´rqÔ”3O“ÌNu•îTv–¶Öö·H1—-õŽ5ÑX/¶9Yy™¹yh×9:ê·xòxø²ÚºTºÛû<< Z¼œZ[ØËU{»Üý>>š\^ú¼:½k]îºþ`@†è Lv¯X¾GÄøõãfbD‰Ÿ¤h !0…[ö±ÛxdH‘ñ,Ž4•ñÕÇq ù©4ùfÌ\%ezB©Êå•þŽèüÕôù¨ÍžAÝÄ”ÓÊN|C‰6uú´ M¨„ŒJBZEiB¦S¹võšfëW?U3…Å’U£Y±kÙ:•Ú¶Òš†W© M©n^½#ßîÕ@nK¼XYzìqb€}ŸÜÐa¥l†6¶|™äaÌPȪ̱0ÏÏ›I—nÆØtÅÇs5K±‹³ujÙ³A¡¦­¤3ºS^}xpª¿…ç66zådÑ™7GE<¸ñ`Лô¶JÝyvíI±Ó–þf·ëÐK‘o7~eùóßÝ„Ÿ6^«zôóé;±­}»¨ðÓʯÿ@"îË.?6Ü㌿»ü Aút®ÀÈöS޼-¼Pˆþ›‹ð@Õ(ŒÃÔ9»Ã-AØ‘EïN”ÍÄÅû°¿m4ÄâVLÆ÷hTðÆ !|1µ%ƒ¬C!•Ô+Çèv¤ìHÖz\’J¸šÎÈ ‘$²Ê.›ºò¶,[IÑ·)½èýÖ^ ñ½W_aû=x¿tG]Þv v¸\„%Ma^Vu` Žx⎹«øØ‹oÍãÙõe޼Š\0d}–ä‘Sžù=ëe8_ˆOöU[š}Ôf€q&Xç†cW·•Žö_–E6f¨M6zéªTÚ²–“ëyꜭþÚ„¦/ÓÚ1¤#wg°—;ë§aîšè£¹VÛj¶#[§—Çúmº«¶[1¼¹3{º ŸÑ;iÃýðÄ'Œpð°vvïÅׯún·+/Úkž÷µÜçÆ{¼.ÄÏæ{sÐgÝ/ÒUŽþ¼½ÉM/\õŸYßËõç–ûóÚQ¾IÍG›êdw÷½càóÊÝÇãïù‰•·RøÓá֘󸣗xzJ€@€ à‰ %A—êiG}xö­2Î;埄<·O_qŸ@ƒ`GÐ4‚ÈÉÌCìôs½’ ) ‰’à1ׯîQ¤ ÀcØïjCÃïFBŽÙH ›Rá¦:•¿ƒ]p"Ì÷#˜9 ôóÂÅCQ®}÷;œ cr€!È€8 !H ú$—? ¾Ï„ikÑ×'Ä U&`F ÁèÀ20€Hþ1„>ÜôžWÂq±Š^bi 0Æ!”ñŒJA>>0‹Åë!·8»<ê1l`| Ž8„$j€ ˜aé2HvŸ”’ð;"ò †y@h¨M¦ e‰EÏÙ±Ž'l¤(“4³ 0˜ @€$M’Aò a\ ÉI\.ò™ž”cˆJÙ@/¦R"ûÓ€V@üoÈÄ<€7Âñpµ”ZöFxK]þБ¦„d$O “ï…o|C§"`€`ÁÕÙ·Dv.—Z„'/‰y.lzE‡ƒb¨ãiÍ U”õŒ×C»ÑêT4£ÄCè.§©:ŽrÅþ£µ*éàVj!ŒŽòš ^ASçN…Î1š"zi//§QÜQQžîë";yGÂTˆ'MP-jSEJ³“:=*Om'Óå15¤B}¤eÁ©Zu[J…JJ›õUŠN”J;+ÓÖʲ¢­Z *)Í:O±>å­IPëY[Ú ½Úµ­kÉ+þ*צ¢°®¼«[°ŠT§Žª9¥fbõ¸Ø/5–ªpíëë6;"ÊÆÔ§3íló¢Z¶Ñþ§°• ¬X{„Ôµ«ýlRWû•ÖN1®\]§ls Ú Ý6}½=¨ö+ÜûY–(À½šqúT©27zÈ ŠrÝȾó¹§ýštBÝ/Z§þØ,xkÇÝŸx7’äÍ›z;ÛãÖ¢˜ ¥v%Jßù¼–¶¡½*MƒÛÝ>–¸$-­IáÛQùòuÀ,M°KÝ»=óú½øp;‹kßž¶—¿¶l.dÃ{ÓñZ¸ªú¥^†‡:\ CS¼’°ƒ ŒÒ‹4À×õ°ŠÝ/•Ä[õ¯A'\Ô /xq®I„k,cç~È~²LˆÌ^ÈGEöÝ’cÒd(ëUÊH–,EÜ+'Yw`ЊmÜe·¾Ø°YÕ-¼eÐQ&_vó“ÅŒÚ,sÃW&윃Mç!áþ¹ÃOÞs ?ýæÖþÈÏ{ô‡'ÝÞ`WFÓCôÐqÂ^¯:Õ}ŽòUc½dñ¿ÎÙ¥cîc7; ä~ö¢Ç÷åŽ]w²g.nqôï{_Ú‡¬ö¶[ÝîbxÞËNù¹»\ëÁÍür7lËÏKñ)‹:H¦Nóµ›ÞˆÿƒêEÏx&;¾tŸÇ{çgÁz…~ë7pà3;û»ïYö3Á½/†Ÿ<×Wö‘¯{À—ϧâÃØðå=~œ“Ï|·[òúþXžÏ½éºúoþi¯«î;úÖïÕ½‹y?ßß»öü¢Ž¿±Á]fº__üø'ÿûoa{âo?÷î/ûòo÷Oò:nþ¾þÍìóøOóó ÐóÊ/æJLýíb¯üü¯8úØ-ÖÖÇ6ðü<°öpðêÔ¾ÏÔÂÏÇOééP°ð@PúDÑÚÁdÐ÷xPøðÿ(ЂXpátð /㢯æh0 mÏÐá#0 û K.ý a0ì¸ß„pLýdnqÐÒŒ0ͯ‘0 :íÓ<-Ôèokã ¹eô.¢ô”ðôô0ZÍwÐ + åå)"p‘0( ×zí×Ò°÷V¯ÊwÎc0Ëj «‹7+±‘ÌpÖ:‘%ð»J± ƒÐ¦'âÙé2Q³VþQ +PÇò QOi±{ÑÇÐYL¡ e±øŠQÿ²n_Iï»p¥±1q¿°å0ÓLà ßp~â0 Ÿ£S‘ ‘Q •±ãn_Ï[m±,/ ñp±P•ï1¡­yíÍoG®m±Ö´1ýRpõ‘»å--"c¥ »î ’ RwÑ9ñ2ëe"ÿ¢"Óî"ñq!×P5¿oo$Ó-&)&’×±_ò;ò?R%R$‘&·°5Q³û’•råfräœ2=jr#o’)­'UŠ¡2‹ÒËþÚ‘0J²ñN$S² [2Íòñª’µ²ñØòc¤-9R'Ï‘+±.“Ñ'Ër%ƒ2/£’(¯Ò(³ò(÷1) óãÜù³tÀòõÄò'CÒ%ï2³/·0»R1U†1‘Ï1õ(#ó2í24ñ’'í1!©%3Åc3©¯3Yr/AS-sR2qK(=2&5k._ó,y3- ógs'çò}³mrAVüZÓ2c+KS k#ƒqTS5”³™6Ó*30¡Ó+åR8 Ì:f7?³7Ñó7m²0Ùs)¹s-k³ëÊó£°³Ó=ƒs4'<éó9‰s©ü³¬ìÓµ3þ=os=§²=ô=ó“KS"åÓ1TêÈñ?ÇÓ#û?´)%´-=ô-ÿÒ;gQC)³D!t:ïRE(Ô¾Çò H@†<þ 4Aã’*áS6÷“6+s;ô§XTKC›üGƒŒ  `6À pÔ6”GôJ”C»ÓGýE/ËH÷ÌE™!ƒ¼É’é2 šÊJ³G±ÇLm“,]S=«`NíA½!O™ D¿’4V‰8l¨Àˆ œœ @ÓëMûƳN›³K3 %3ÒºÑï$Ô&•ÃþÆt M-‰€ŒŠˆ ƒ¨4"þuOãô@UO+uH·ô hÕR‰t8Þ‘ÕJAW³MT§ˆT•AŒÈÈŒŒ €>ŽéUMôQ_+XÔV­US± ZwNË 0r…Ò¶"ÿt1I£È(iJ`Y›õY«TZ1´'†\e5KõsD•à^±´[snú`…4D1È(U‹`šIZµœU(@b'–b+Öb)`ˆN<`:Öc?dCÖc=€NÌg.e/6c7`cEÖe_–dçÄdS–f'veé^VgA6fådfk–fovN8vg‹¶gAàge…VNˆ¶huöhAàžvg6i•Öbþ™œ–jE6j¹¶ky¶dàd±Ve5vNr6l½¶NÀvm;ÖjÉÖlÏ–e‡ömCökïVle¶PIXiÕP‰Q•Q‹À||Uq—q×qr#Wr'—r+×r/s3Ws7—s;×s?rûT1ÊtÐÔÔ”ØÔH`¨S_vcWvg—vk×vowsWww—w{×wxƒWx‡—xc÷6}Hc`›þÇ›V©Q›ôI£4wž®{§Fà ÕWh4|n4{Ë×|Ï}ÓW}×—}½a{e3œÕÕ£"`@tay i ô¢~ï7‡àþ{ %QõÂÀŸ€Jö·ú=’Ôy7ÃÌÈu?óB`þçˆ`n”œ4yábƒ;>XŸÀ€äࢠ>€`J… lÔFxJx;Œ©t ƒý¢‚¡u3H …‰ À~F "1X……À71|¸8˜Š™X ŠÏCPW“ÁY3¤¸]ßµt x/ÐX€­3 ×]™uÑã`Q“.£‚Ã'ÀzÃM@”ˆ" †X,ÙªX` À‡ý‚Vu—h ‘ƒ#]ÿ(YÇ&ÖH \“Xʸ]µQõþB‘3< Ä'“×¢XØPyTù< `•]Iƒ ’#qYt¹ñ7 õ—õBù–ã5— ’ƒ’6M`–Ù"+¹Y’M Àã™Ù5‰,™ŸÄÙ<ÂXW6V©›×"ç˜pÙ˜•ÉùÌÙ/žyŠ…@‘Éêy6€X8H6‚y˜K9 —¸‰Ÿ8Šù¹•ábQX‰¹X@£ÍC“šWš/Ã$€> ”G¹1:€È8€òd :€„÷‚¥]¦K`dØ:À Z/u?à€ 5„kú¦ÑãžÄÇŒlø‰ Xþs¡¥QøŸØ/®úPF€  @ ú) QžÁ!¸}ß®ãZ®çš®ëÚ®ï¯óZ¯÷š¯ûÚ¯ÿ°[°›° Û°±ƒdü BaE×. Û²ó‚€Œzæ1Nª¹Ÿà²G{-Œ Ð PÛPÛŸIÛµ§b€ ežy¸lØæôlx$`€V™àŸ`ª8–·MÀv 4z1àƺ9Ÿ9jy y¥—_Û»B@ .à±…  ·=À± 5‡Ç§8èpK`Q ¦…Žª¸^G÷gþ2À¶™Žy€œD Œ¼É¼ àÊ)D;l4Û…¿›Â}¢ˆ…€¶…À¢W ¼–KX”錾xµŠŠ—T†©²…`•jõ9²1¼“Mào‡àÁ+ÇkâÂMÀ¼‡ N5ÅýhU‡à™Ãi€R×–˜Å¹™„^hI\—Ô¼!x€’÷Æs\ËM—3ÜŽu »³\Ô4œÌ{­ŽÏÆ ù¹A8aŸ¹„[ !|Ëïü"< ŽÉ˸ÉOU@Õ¼üY턊×Üɇ ‚à\Á©yèÜÈÏ+"v¼ÏAUÌ…àÃ… I œÞüª¸T\Ÿá·Ñ‡€ºe¼„ͧQ‡ÜÒc] p¹ÇÑ[ÓOõ½9 ¾¯ºQȉ"@…á™à¿|ÆM Áe„$ È[Ö«]ôœÏÏûË5½»oY¸ „:`–J‰Ý”›¹{[Õ—=ÆyÒ…@ÒC@¸ Û­ß_¢p§`ßóÝß ”;›£@¹ÿá÷"Æš{%» ú=á#^â'žâ+Þâ/ã3^ã7žã;Þã?äC^äGžäKÞäOåS^åWžå[Þå_æc^ægžækÞæoçs^çq%!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbblllttt~~~ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›³Dx D§þƒ!>ø %h%<Ó 4©£83’ÐÀÀ@€4$Ù «W¯B4| ©;‡ uº6ŠY6   À@„Lº@D’·hhŠêÙ´Q3f0À4@â‚e –` $fË$hæü§„=›0:µS'¬ÙD€A„hsX¢×_¼HŸLøL["ÂWôÐȾ»› ÐÉôê^J9~„; „ Â!€Jh)ÀopÕg vÆ»‰äÊ5b¡IÆÚñSRxÄ ÀGDV  K ñA\jDþæáN!0på€ ž˜\ á|÷í„¢‚ ŒG Pâ_´WÄb¤À^8hR"0  ‰Fl€ÀU``àZ3pvB@¸à„Æøš *È`k^Ž •Xf!% ÀxciE%dhæ‡Ñ~4A‹Iì™!%,@æd€„W9A ¹§„pÀ†,ºHD—o}Ȱ€˜ZO"Á váŸC`Ê©¦6€!H´YxDùÀŽGô¦a‘<.€¨æm@ÄlÀê´¬8`°B“Â(œX(ÊcÀI+Àþ·^KÄŒi+)qìyh㙋®µ¿ÚYQ ΛD Wé:ÀL„ º´`™,’& QÂtVúbq€q‡y ]Úé„!rªi|©·]a0Ä´õ'W!1VGàê2]…†Ùg‘œÈCŒ,@k,Hæ¸;›l© lf!Å­]^q&„à0Ä’ ýtÄC\X±Ó͇V¯^µÄúF4Û[IWpÑ­qÀð³¥)b€ìâ<õ|>-D—Çq•Nh®Zé4€ãªžL„|BÁhZ8ŽUý"1›ÌÁŽ5–Ð16>0»Êš8ÿ™}õþ¥ñzسo®ZßEÈZrî‡×n;­NÊš›0€ðmC´ßØK\—„ôLp€@ w;^DnøÚt q@ÁDœ1Ÿ+s¬Ú‡ !VžhÁ½"ú­²<ÙÀVQèùòÖepÔã+ÔI«4__†õ»û áI tÅrîýì{ZH¶!)ÐHæ;žZºF6ï!@T´BÆ<¸žœeº® Ç€ö@8Ë sÇÓ”‰Š5žFwN)ÜÆó¥g-ñš\tf‚±ÜPX—"Ö{Ð5ÄäP{ÓñKr¦#gíeN¡a Ñ3FšÀŒú›OrþŽHÆ6äy*ìýŒ°'Ã-¡Ør£½í-p`³[U€F.ΉDHàSæC!0kB€ä“Gðp0‹þ*[¥Å#ï‹ ‘X„:ŽO‚«¹¤ ™ûhf¹eå/µÒvLÈ~ ­ÆtÂH°r{JSXG„ Íç?hÓ ŒŽÓ1ÕpÎqc¥sTóŸ×h“Ul_úÞ¡’bB"’gNâ OŠçŒoé:H‘EB4yô¥:ö³x“m Ì`fÄ4¥;g&·#ÐM™¥Ê3³ÆHX–à†`bdÕr3dÍbã ¨‡þV³‘uΗ(sŠI«–Rtrê‰G&ZžóÄÓ9DÝÇZJÏ©  j²jZ/M(¶ª• k"è¨>*ÂânlV+©&«jƒ:´ \Yh|&¸ß”:ÓC+>`#e nCØ€yl% šíÚV·^Ö®n*ŽÇÖ÷,X¥ Ñ™äk`&!¿6 Cتò8Y„Ùlæ6¹ @t’0ÊÕòH¯BÀcDÕÉYiËY{³A³©½Š°r­M  »&‡ŸÃk–k»:ÌŒ‡¡_í‚üº „ $ “¦³ª‡¥d!¤Ë ;æR„RKj“æ0ÀþžH@¼êsЦBp¦åÂä]PJÄ÷fò¹‡£l@àö8ì.L0Ïz>[¤°wIÒ´”¤„N¯jIlrÕ—B°]ºÀ»\Å­0°&€(JêJäz©áàb¡ÈÀ80¾PZÌØ€ÆR‘¨K‰ M܆A!¿CXÌqßøa”àx#>×fy‡ܵN&Â~P÷‘ y$_àÊd6 Ÿ<£æb‘Ô—#3ž‹•²Ûe8XkÈø#ÝÄ–Üæ:Ç<†:B^s!:ÛùÏ+›54æþ ™¢ÍèF;úÑŽ´¤'MéJ[úҘδ¦7ÍéþN{úÓ µ¨GMêR›úÔ¨Nµª=ý ä0ó#”Ÿl‚)#°†¸Îµ®wÍë^ûú×À¶°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Ík„Ff¬*_3øbáBXµ¸ÇMîG# ÜèN·ªÏ­îv»Ôì~·¼ç]i®Ôûa@¢?hÛ‘o50Þ@uo€€ÛØà²4 †?ãàp††fL7<ÝhIe†x6þ Ž;ÃãÌÐ8úЛ>ð¥zêOÏúÎ/Ãõ¬':ìW¾ú؇~öÈÀ½íë¬ûš×~÷–ï}1„|Ç/¾C“ æ+ÿ„ÎÿEôŸ¯¯é÷ÂúÔWöw±ýìã¤û¹¿÷k"þ[”ü29-Ôþ—°ïo?Kâ úË?%öEþïo’ý·Âÿü7¸ € Q€©€€Ø xþ ¸€ñ€¥ x8 X‘¡Àø !ø1‚`‚$¸(¸ +˜‚Ñ‚™ƒ.X2x 58ƒqƒ• ƒ8ø<8 ?؃ü„‘@„B˜FøIx„ö°„à„L8P¸S…#÷{M‡…VØeU˜]¸…¤qa†O÷…‡`†dxtZ8uk˜†Ë׆Y‡n}røuu8‡Õw‡e§‡x¨}|¸v؇߈qGˆ‚H~†xw‰xˆé·ˆ}爌è~8x“‰óW‰ˆ'†–LhX¸‰Ûð‰ƒ Š ¨ˆš¨z§XŠy˜ŠjÈŠªè‡®˜…±øŠƒ8‹lþh‹´ˆˆ¸‡»˜‹Ø‹vŒ¾(‰Â¸‡Å8Œ—xŒ€¨ŒÈˆ˜èxÏØŒ “ÇŒÒØÔ˜yÙxAŠàܨ àøãŽÇPŽ}€ŽæH ê¸í¸ŽÍ·žgðøï˜÷X×'¯ÇúX‚þH{ôø‘w`‰ Y ™ë¹‘y 99‘õ'‘¾7YÙ‘ÿ§‘ÃG< ¶Q"É!ù—l™’ø°’nppP6y“8™“:Y@’0Y2ÙGP”Fy”H™”P>ù“ã”pD©”T™”Lù’NIP¹CY•^i”þW™•±•]'•_ù•a)– A–iЕgY•i©–*Ø”Âà–o©”q)— Á–h`—w‰”y©—I—ñ8• ˜„)˜ÙÀ—KP0-•tkc¡B~y˜E˜ŠIŒ©"@-{ä>þ‰ÚS™–‰™™™ƒ‰i $ð™ïÄŠ¦y˜¨™š±™LК²Wµ›9›´é¶¹­™m ÉÐ%2fi™G雿9„«Y ¸™ ð0ÉÉ›wéœÏ©Á©ÓÙQõIÜù–Þù1¤0žM $ûµœÌy™ì©žÍžIðeÄ©ðIG   `˜ÝYŸöi ÍÄþ >ØàžKPžñI Ì™žª•ú ÀbÑÀÚÁ.À±À€Ú¡ó –Z¡ç¸¢C§ÓOÀ±-WѺdpò9ŸÊ¢ñ€Ÿapžg¹£<ú> @Š–.:¤u™¤¶p¤^)¤J:†W(¡§É¤Qê EúN —Vz¥¼¥i—£Ú¥^ª `Ú[J•PZ¦çp¦\¦xI¦lj~rš‘T*›u:§´à¦[§V™§z* |ÊxbZ¥X¨x¨Óৈ©¨ˆº¨€ê ŒÚœ‘ú¨#é¨Ðx§½Y©–J€œê©šZ ˜Ú©Ú8ª“ªèù©¤ê€ªš€…Ч¦ºªâþت¨0©*«²‘¸êy¨¤´š«ø«¦`«ô¹«ÀÊŽÂ:¯º©Æz¬KÚ¬´×«H ­Î*}ÉJ ĺ”×Z­1¸­¢­kÊ­Õ0¨Y®Þ*®;x® `®ÔŠ®fª®"¸¬¢ê®O ¯žÀ®ôÊ‹=*¯©Ú®ùÚ¤öz‚üê«þú¯{°œ€¯kŒû*­Oа ‹äúv;­Û { {±…X°Èê°\ê±{© ›¢Å:²[²&®(Û¢"»¤ «¦Û²ß8³Ý³qú²4˪: »³Ú³Öг*´@û­6‹ ?{´Ð±V°´L[ª*›¢,µÖj´ûH´jµþЈµ_Z±ëµ\« N[P;¶9µ:š´hKeKgÛ¶©¶c*¶r›®v~`²wÛy«{+³Û·ð¶—¸9K¸i;¥&«­ƒ«¸Oȶ–·{µtk¨•;·Œ»²’›¹mÙ¹”@¹žË}  „ˆ[´£‹|¥+ ¢›º€û¸©µ” »®K†[w§»µµk¹›Kµ«»»Zú»…›»³ ¼X*¼JH¼·j¼ûH»ð§¼'˼¤ë¼‚ ½Ž+½Ó{¹°Š½ïJ½vÚ¸U˽Ê꽰кâK²½»¶£Z¢†’ç{uÈëæû´ÊF¾µ{»Q0¿f+•;Ù¿;Ù“öëºø ú þ·Pà¿ô:ÀOPÀ‡‹ÀœÀ©ËÀNàÀ¸ Á¾+Á£KÁM`Áù{À ¬ÁžËÁ¢'»Ëû£ Áï‹$¼LÀ)œÁ+| Ì/ÜÀ1¬¾3u5L…Ö¾œÃu»Ã ÙÊpÃ,ĘKÄiÄ^øÃÑxp¬ÃL,-¬HÄS<ÄU ’N,±P<ªRÂ]ìÅ"¬ Y,z[¼Äe<“_|†a,c¬ÂmìÆgìª&½FªÄÛ[ÇBùƆÆ.ÌÇÌêÇ|ǵÇ(¼Æ}lÈe‰È¬šÇ×»ÈdìȬ½…LÉtlÉ} Èž¨È{ÌÈ™ÌÉgpÅI ÈXLÈóJÊ¥ìɶþ ÊÁ+Ê«ÌÊeð‘%  °~Á7*ǰ¦²Ü¯´lIÎáãr6v¿Œ¦ª,ÌÃÌÌ\r>ܦ´ÆÌ’ Äj\ÉÑìËÓL€3ÜæÐÉÛÅÏL°ß<%RÜÖçÜÊé,Æël±íŒÂá<`)ÜvÙìÎÍü¦ù¶ûÌÏÀpÐò ƒFÐ÷ìËÁÌÎ ¼ý,2+Msû[Ñš,æ=à Ð>çË"ÊÞ\Ò}êÊ£XÐ3}Ñú,Ó3}ÒÁúÒ±Ó:M±<ý‚!ò±úµÇ> Ì@Ôô;Ônð Õ $ÀEþÝÔÓíÔåJÓW@àgFà˜™›‡¦\.†Ê§|Ð|ëÕB} 0ÐG ÐGÿpÀ(¡ÄÖ”áÖ‚ ×qm æüÙBÖm(ºÉ?ÍØ‚}¸P=%Ðt\®9Q6¶V46M¨\ýÖmÀ‘Í € pÚbONpØH0…æ/~}sLÒ¡ ÙÀ`ÚÛw]pâyÙ±$g}æŸýÕŸصmÛ×à ‹Ägª­œKíÌŸǻ£=Ê})¯-ÜϽÕZ=݃\ÝgÀoâýÀoåÍÛÈû‰Ù´¶ÙÊ  J´Ü<ÈѺ޽ º `ݼfؾmbqþ\üâmÚÝÝžmà÷-ÛûÍÐàäýàåÝàM¡Ú¡Ú/c{÷Û¨Wߺ›àA Þ£_ì"AÉ•#k=Ü âÅ+â#ŽÉ³ÜØ´-ã©Lâi@ðãSÎþâCŽã8ªãÆ¡ ŒÑ<$äŽ ÝEŽãéדA%É™ÕOÎÝ[näÂäfP»p¢áä7ÎÔ]îåÊ æeÀáU´#…mÏQ¾¿0~Âj®àlN°°3À<µìâ ØÒ}稗çcà&ppœfNÅ0æj~‘ °%0.ÏBeaºÝ7=ç"~‘U¯¦ågå”îåþ ¥èž~à©nä™™åÒë þÀ»~ß!XÆÐË(ëÄÝëÞ-“ØÃ( eê’nãÏnèœèg ‘ÎÅ“~êÒ’W>áRfÅŽì0\çz,íÓ €/­î¾JMî8lîéþ ³Y.ç³N¿ò>ßp}‘Í’‚Îïð^Û¦üׯNäû.ã¯à ?èý¾à–Úð¨÷ð¿ð¡.ñ~àáññÔ±¿ØÜþéŸà!_à%ÁÚ'ÏÙOç+ÿØ-ïÜ1Oß5ÿó)Ÿì9¿t;oÀOï_NãмíÑŽî3£//ô7ïÕKÏñÂ=ôþD¿æFÑH¯íDõAÏë=?Ý\ßô^?òwöOÿ×T_õfÿõñ~öA½ödìloð?ïŽ]¯òsÏòu¯RÏÙi¿õ{wÿÁOïpŸô\÷ªø,<øåîö:}øZæŠOëŒOÃb÷•/å—ÏÙOø/Ó’ÏÆÐ>ùJßùEüùŸ÷2únëømÏú‚=ú\ú¤oø®ßĪû›Ïð¹oŰŸÄ…ú_„Á¯Å¡_Ò´?ʶ_û¸_üûûŸü½ü5NùˆoèÖôÍÏüÏõ9Ýý×OüàßÕYû䟾¦ŸøÙ_ö¿ïÅÒü²óП¼ñßÍóõï¿÷oþóùïÔ@€LEã™T.™Mç•N©Õ¤bÑn¹]¯¥2´J…•ïù&ŽÙm÷—ÏéuûŸ×ïùSáº/Pp ípK-¯ Q‘2Rr’²Òò3óéO³Ó“ «íñŽqô¬ôs•µÕõ6“S¶–Ò5M ï4·KÕ6Xx˜¸Ø’öXy×— ˜®×Y zÙú;[»5yÛÛ©yl×ÀLœšü{½Ýý=ª~=|ºZNÚ^}ž¿ßÿß–<€×ê9»'ŸÁ}6tøP@ˆÄ ú:'¡Å…9vôøñˆD±*æºø&£É#Y¶tÉNäËU%Qt“²æJþ™;yö”Óç¬PèÒ²ƒs”Í K™6Ô©$šIub4GtœÑ¨[¹veVÕëÓ¡D•²AÚ¨lXµkÕBeq,º´VÎ:ûo^¦nõÖ™Šön›º‡æö5|˜#_ÄVEÉ lö*ÙÇ‹)Wö§Øò˜¿vµF‹ì¸sfÑ£áa& ª±¸Â…>«ž|vla¦e7ÙLøu•Á¤r×öý»màHn󯵾ãÙ7ºÜ9“â©zûI®ú› ¸w÷þü÷ÑÉ“î|º®ì7¯k\/@|ùóé×§ÿ¾|þµç›§÷²Úºs@[$” <Á¬€‚êô{p/™óï ¡Ø:þü4kO% !ü0BÉ£ð 7á0'YÐ5A|Q&þ&Œ«EYTÎF¬„‘Ç‘dŽÄDLEª\åFìz\2ª ²¨%;2ž"£’É,rò7(³’r@^¬äLË2yâÒ7/w¬2I÷À¬ÑÌ8[B³65‡´mLܰ<±Íåü$:e³sO"ûLñM]t"Ac#4Q%Å<ÔHF-uÈQØ TÇ;¥ËÓ¸KE(ÓÓ6-‡Ò+#usÔVç)Õ<EµÓBñL•LWumÖÑN= Ô Wõs×b½éU´_ý V½a5ZlÍLYÏpÕÓÙJ£ÝV™i-«ö«kCåtþJnÍ-ÆÛÊÀEN\aÉ óÜxkI—²ubö?O—À°Yyý……ÞÅ쵪Ý~i-÷ß„Y ±Q·B[?-8_‰¾Ø3‹£*0€ƒ%6¨âd•ô`x¥8bŒ]~.¶ÐàƒØ@ Є¸/¤“YMN¢g}éHn©ƒ›X`€œ«r˜=–KÔø ˆ¯NškdôuêŽà€’:ªá³ZH¬Mf;Ê®åîci–8àˆÈ @€BÈ:µ£—…ûËw‹ž›ñrÜ^ªŽ ’ð@‚>È`€ºp”W®5[U?}Ùǃ2@r#(·þ¼ hÀäЇa£EGwöT÷ ¼Ð[ƒ'tÒ7ÜZå{²ß{z €#Ð6y#„ î°Ûåwo>wèÍwbÔg¯›j:ä" ~BvÚ“/ÿpçqWùüþ‰”¾'2Ó€¨‡3ØlòÃ<€Î}N{B#–în—?üùƒY O8æ1Eˆ€€8@„ÓŸùG¾f†&¨[_ÔÄ­éBZk[ yÁ ÖK‚Ï¢à÷·¸fp†z©á†nè¾·­ðˆþKb^–HÆmˆBŒ"§ˆ—*êæŠ‰Ë¢¶¶ÈE'Æ |Zd¡]ÈÆ2Bþ¯‹où⊠®:¾±wqdËtÇ{5ñ‡xÔ•÷D2‘yv¼  yGȶÒtcŒ$"ÇÇHÔ92,|D’ ÆIKÊ “^Ñ$›,ÕÇÂô'ÜìãU¯²Õv~usOLY{¢ÖR¾52sé;¯™ÂÚ¶&–­jMÛ˜Ó×õ4 ]\^ý:8Ø~ã™;’f6»4ÖÛh¶PÂ}Dn'¦Ø°þvWµýq_âÝæv4ÓÍkI#»×în7¹×ý¼s7ªÞø¾73“xWâà<ü7D¼]ð’æ[ÖåFø¾£¸ð‡4âÕìw0>‰ŽëuÞÚŒ6‡§íç’wšWÅåýì8o¼²0¯­ÌÂò‰K\á!WgÀ žqwÒœ$6¿…ÐchqLñœ‰@Wy·ˆ.•¦oÖå|VþúÓ•ép‚PKßr’§ŽóMZÝZ„ØÏgô-!]ã`o%Ðíu§»ê\W{Õ}NǹƒìÈ;uR´ÿüîË­»´°>ö—½ïCíúâÏBºÐˆ>¹´]±w'~«·ûà¿Îù*Uôé=¼ÞGÏ÷¨7z䟥s¨½køP Ô¥>u„‡LëÒ_þô¬N½jW¿äjï¸õÖ¶÷±3aù೟õÛ?÷¸Þ½rGê|vOšæG+Û'Oò #Ÿ¯Êïüö7Oÿå¿ùÝ7<úÍ ÿÉÊŸ”Ôϯô¯üðù.øþñÂØÆºð ðü Ïþ<îûd‰éÍE'gîûëçË÷2Mî$pýBðÀ(ÿ°¯ߎýx)EŽ]¯ç^píTÐø0»ÆìÂúZûpPû„°þŒpè:÷NÓ° / p IO Á ‘pþ´0Ÿ0ætpiË`ãñ œ$¯òüµ6?í÷jOÝÀð yPé ô*mñ$ýŽ ÅPЂøâÐ ApýÒL¥@bO f/èöPñä°…Oà¸0 qÃT¯NÂÐq²Ðó¯ =Ðw?‘D׿@1óúðyÔJápø:þÞ4‘ÞPQLT1ùXñÏL®÷ÜP×$ñ)Ñkp‘ØŒXt1þx±ý0ñ ÇPI ‰ÑûQ›¬1c8Q=‘¥°©ð#PéÐo±‘Cÿ‘Ú‚1gq]0鯝ñï%×0ßqiqç1˱]qPê1«+qG §Ñð!“ð“¿"g+!‹0"·p#»p!E°"Ÿñ1$ñQùq;R!ò CÑ _5å%Ññ$e±ãñUR#q"yR"ëÐò ³$uÒ ?r!Q(a²e²$­â"Ùc:烖€Â&ÀØmþ™Ò½åÀ’òÄÒý¸a&3ï)&*×A€jÆi’  `6À ¶²vöÑ&SÒ'9²/=’%Û°)Ir"Õò4šÆ€L`~’ v!.@…”Ò%*1Ýq/¹Ñ(ч¿r*ÓöÒÒwNƒzƦl’` à0è~&S/©uDòá%Ù 6-Ûá6?³0C“42‡gg’ pŽ ið2‚ºR6Y/f7›33Á1:™à9ió2/¡ Í0<­:™í,¡ò4"gr*' Žà ~’ó5GÒ:as3Ãr9aº“/k³ïÒŒÂ>ùm)i’4XgrvŽþÎÓÒs=EP9qÓ=o’ˆúS§‘Ò$4'ÿrå`OŠ/4 A>ÖÒî&o† À~Šà8‘ =€bTFg”Fk”&àf¼Ã6€G{ÔGH{Ô¼cd&ÀFÔFqtt4H›ÔI‡´;ŠI§TF•Ô;ÀI³ôG¡”;¤”J§ÔJ»cGµ”L¹¼ôK4L¹cLÉ4KÍÀMµÔLÑ4MktMA Mç4HátOùtK‰ŒôN“4G»KµO¿ãO•GëtP ÕP—TLHýÔR5JK“4 ΉlÌ N35W FF?SUUW•U[ÕU_þVcUVg•VkÕVoWsUWw•W{ÕW_Õ3-~ž&jÓ1 8€6Àg´ÓYŸZ£UZ§•Z«ÕZ¯[³U[·•[»Õ[¿\ÃU\ÇZ?`Jæ4 Èf ˆzX3.ç².@T%Ê^ïu ¦2`=ÀT‹à*=F+ñ•` Ö`aVaöôµ*G#=-2J d$€8ÔuvRÈ0(Öb1Öüu>PÓ0<ÀHhÀ(@@c€c?¤-Û•4< ršõа/D@Àf.àdG+9@.ÑU/t–gÀgA(z†; H/2à8àî²B +þ=@h€hõcXågp1hvAGƒö@€f ãl“¶ z*£kK`gçvm‹@vÞBFÓȆD³AAc#n ÌóH17dëÖÄÖ0H5qT~HöALT8‹G4hÖc`êÕ2×D`oŽ >µqÑöqÿf  k·8M—oŒ u%—9ÔuÈ3Y"@s4À9í–uM p‹ T“5 ƒtM 0À0çcp÷-`iW=KuyóÃ`< ô4J`„gt‹÷x‹@yWd•€lj1$àt±wAU3vÉ#xN”scÃf¨/H—vQWu£þyËÓãx×ôÆ}MàvÿrM TkÃSù÷-H·rs™·xárvã€å¶÷i €‚}ãk/´X w2˜2šWmÙÖmávƒ‘ }ã€××^˜o‡õãxØ5{GÃ$€>x…WX9@g€8`¦l-ªp`p­¡êƒ5Y¶„\1ê˜>F€  @Jx+À>´§e_–aÙ‘’#Y’'™’+Ù’/“3Y“7™“;Ù“?”CY”G™”KÙ”-Eh™þ€L7X•à:à”g¹/ζÈxq;êW àh˜ßâ|—À§ ˜#w  €ƒ™™»Bdãcà×R¶ªÖ*óªv$@g–— „è6Ô2À²Ù„Ö×t1à¹L—sá7¬w]éÃ{›™Ÿ—Bt@ Ø `…é²8kA¦ào(yज?‡n z·Rf <”o`€D€r è€ àÈ—û+TV ûY¦}‚l‹@š‹ sàU~Ù¬—hƒ7(Ço#÷„æö-‹ }d¹¨'…L7ƒ]Ù¦ØS ¥þgÚªy¢¦M€ ‹ ’Õ uŠÓà7tf ÆCmÑÀ=Æt‡ºtß~_Vgеª¯¯[âxoÚÄÓ`ŸïÚyMõ€ù€X1ŸútÛùgQ~‰¶›C˜§óš²? ~ø:e‚Snó€ËZ=Ácn›­€fqæu¥²ÿU°+[¶"«5û6»|yI([ è ’„LÕ·Czž]ÛF†5`™gÛ¹â0»¶º¯o;8šºŽYS5!€8 ’pŸF8Ú£Aºt t< Hàµ9 bú¹ë»j›•¹ú¶÷ÙxÁYŠ"@5Ï©4VEñr¼‘:µ™8®[ûµY3ÀøÛ¾+ü%· 0ÜÂ71 €.9 ¡ó— ЙÃKü0" ÷õ•£@ÃMÜÅ_Æc\ÆgœÆkÜÆoÇs\ÇwœÇ{ÜÇȃ\ȇœÈ‹ÜÈÉ“\É—œÉ›ÜÉŸÊ£\ʧœÊ«Üʯ\^‚!ùd, X… 1"""+++444<<<Mci|CCCLLLTTT]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@”pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Hˆ D'þÏž ŧ%£i ‚ÔMSœ'4@€ &–x€ @P%ØiS¤%vJyŠF„U L(ÑDì‘H$a[f@¦jϰå ub(H8@€Þ$â~EB"€e²€N ‹¢©æ¬CÑÌ9ÍL˜À8€&và-m„𿀚I«hïÂO¡0 '@7Ò@.`„“ᄺâFñà5Ä’ØE  ]Ä:ܾјOœ£š„XND€žwñ€ê€  ±AdHpz((GA,ÅÞY“ù4‚–!>‘ð€þ„Д‚áý囆6  PuÀ…”¡¦]`ÀwÜ5Þ^;ð@…,ÑAW tC‘‡|#"ÈþWšN”°a¼FÄ€x ƒÄ‰%f¡E@"–J¶WQ~4¡QEŒÀ(] ( fFp€<À¥I0¨p@ƒC(—Aˆ¹±W&P Ðx¥eh]0A"|À£D ˜ hc#(*¨‘7qp2@G v„x²qjª˜zE¦§84@dZ•"ðªe=j¬¢¦Š†BŒ*(¡B0›êxí¡‰.*bþ†Ahç­³áÞª&DP0è&}(œp€ÂÙ©èÙ²Gèd@iÊ= Ä|€v¾&8*ƒ"¢`„?éB¼'€t/©Ž2X±Çm a_ø˜/ Œ…¬-#!»ìRÀ®W½õñ"T%\]è9{L* À÷0 11Õ'w$Ã8ÚÅ(KÂ|HŒoÆ•ÕúõÓÊÅ7¯Cx‰e¡ó©]'"‘ßI|€h |Äii!¨p0ìÔÏ÷´NÖÔÆ‰Bâwê4€åM9)PûŠšå4€²¸qZÌIˆéº˜ÚþÈDÀJ&æDŒ€5PC.€‰/>öî“ aháBP(\Z½£0Àï—+NÄ¢ÿ¦SóÏ¿ ‘  +|oñÈw󄜀¾åE0FÁû=ã›÷±qñ)QÚ’‹fV²EðÈÒⶬåoÕòšÕpÍè¨CÂi~³÷q<ú»QuH?ÅŒ1A HÖµ"àKÈ¢Íýø§|Mà}ð“_h(„¢€1äéRÖ€BCt…}Ú[H ª"ŸåœÀ¡Ó›~˜à¤O Èz˜¼#ö1jjs^Æ(§›ÿ©M9£OçÀ†@þ©ƒCP Í–hœ áuÞAþEÂÚ”`DAM˜I ¡f(¡Ê²&ÔHÑ;Tâc Ù'-6 âCpÈ„ð á~†Ì¤Xȇ„À@$cB)Úxó1ÙàÈâO'¦ëàÐ\)©T•;”c,ˈâôP„©+Â!€:>Bðà ‹i+þ7ìã%u(©Q&a=>¬Q4I¼ÁXˇ$D8DÊé1èL'cMáë(“p¼$ÏœÓ]o®w2Žuktµ“-5&ÝÕŠŸÿü 2õØ<ÀàE[’v¥Á#¤…vC°]ôøš-~Ó†÷õ$UÏ=S›!eœèÔþImг ÈIS#Ôm XÁ„°AN€h¿âˆ´ªõ‹1;AU8s‚¥U,-Dñç×ÈW´8ó˜º9‡$¶´Va¡2q‚ºîLT6ÒLØ&‰f™£QMb‚jÚGµI6‰!5k@­ã âØÒ©•Íjg»äBÙÍ—D¥‹À§Ä3€Îf;è¬Y ¨^²ñ!8mzJÊxE5p€U† -DÀ\DÈO$PDî'æù_³À¥©\>Eµˆ@k¯8P `I8 |VÓwêÌ¥”,CxmªX¦M‘új ‚SÃìeÏÙÕ±vV»]mGGÜßNw>ÌÝw¯»Þíwrô}ÃßÅ1øÀ^ …GâÏx2,Þo¼ä½ynT~ò˜ÇÂåµ±ùÌ{^ ÇFè?Oz&ŒÞ§/½êzj´~õ°½4dûÒÓÞé5¯½î5Ÿ{­÷~÷ÀýïÇ>üàßôÅg{òÏ|Ö/_îÏo¾ôoÿ êKðÖÇzô¯üì3ÃûÜ;ø•1þð¯ºüÈ@¿ù3­~c´ýj~?1äÃÒ_÷¯¿$ó þëÿmþç øí1€¼`€¨ ˜þ€6Ñ€¸8h 80q´ Ø( Ø*‚°@‚"x&è )x‚$±‚¬à‚,0¨ 3ƒQƒ¨€ƒ6¸:h =¸ƒñƒ¤ „@XD( GX„‘„ À„JøNè Qø„ 1…œ`…T˜X¨ [˜…Ñ…˜†^8bh e8†q†” †hØl( o؆ú‡@‡rxvèyx‡ô°‡Œà‡|€¨ƒˆ¾çsíPˆ†H|ˆÈˆ‹¨wŠx‘øˆÐ׈ë0‰”ˆwÛW˜˜‰~·‰Ÿh‰ž˜g8¥8ŠŠŠãpЍyªHx¯ØŠjŠ€@‹²þÈy±˜Š¢x‹âd‹~à‹¼ˆz¹ø ÀŒ®7Œ®¸‹Æ€ÈØ Å¸ŒÑðŒz Ðè Ôˆ×Xa׌–ÇÚèÙÈwÞø,Žu`ŽäX è8똎ø7Žjòx í8¿p‚Wø8úøÿØ ÈÕyŒÊx1alà ¹ És‘‘ˆW‘„‰{ ¹‘娑Õ'’ 9™'Y’/H’Ú÷‘*‰)y1ù’9È’ßg“4Y…8I~;™“ZØ“é”>ù…Bé~E9”dx”ó§”H™†LùŽ.Ù” 1“e@•Rù V9Yy•Wø”Á°•þ\É…^Ùc–xX–ˆ–fÙ‡jy€m¹–‚ø–•pÉ‘t ‹wY—F(—¹–z9 ~ùù—uÈ—œpR‘_Wåæ:Ü6˜„©‡†¹ $*‰ómCà' ¶&왑ù‡“Ù ÷ƒ™Éô| š„8šœ`dda¡Œ©©šˆ0›®¹˜²„ðBEsyI› a›¯Y @@½9›ÀYÂišµÁhâÚÍIŒÔ© ® F ZÓù›Õù“ÞY ‹†›IÀDðàêéh÷!Oë)C×NP=ÄœîY‹×y à~òàˆ£A   þœÓùpÌvfùÉ–á) …ä:t+œq,–QhÆ„ïqoú é€ŸWðp&z¢(š¢*z°Ÿ Ú "jïaP£6z£8š£p.ú¢»£U0£::¤:Ê£꣜أ¯ ¤DÚ¤5j¤H:‡Jª‚@£NÚ¤P¥ø¤TÀ¤W:¤Yª¥öÀ¥Sà¥_š£a*¦—Vz¦h:¥j ‚pº’mê¦7š¦q—GŠ‹uj§O:§yJ¥{}ê§x¨‡È¦~Š£‡Š¨ŽuUº¨w ¨ŽJƒ”š fº¨Z©!z©5Y¨vº©œzd*| ê¦¢:ªI:¨¢©’ú§¬ªªþ«Âxªgšª²Š—Šúª;꩹ ¥™j¨¾ú«XY¬¥0¬¡Š¬ÆÚ•´Z®úª¸Ú¬ÎȬ£ ¬¨j­Ô†Ú ¬Ñ*©Óº­¸ø¬Çh«_®âÚªäJ‘æz¥èš®Â¸®³÷­šÚ­ð ‡öz¬íê¤ïz¯³º«Òš¯þ*™òôJ¬;°ù(°€­·Ê° »š ;’ûŠ¥±µy±bY±DÚ¯Û’ ®û±„¬Pà°ç:²$ w*{ (ë®-»²`²Oð²ü³2Û4ë6k±›³µ°³Mг‹³@;F ˜»¬?{´rÚ´7ɱ`š´N+ŽP»R[¤T[µþt ´È—µozµ\»’b› D;µe;¶˜ºµ… ¶ŒÊ¶j p+™n;©i·>8·p¶Z{·x;„z+šuk£û·0¸‹À·ak¸ ©ƒ «Œ{“~«ŽK›­“¹Rˆ¸«ù¸½z¹˜ë¬![¯žû¹b9º_Y¹kº¤ë²š›Šû¶ª»º•àµKðºv+»F»iɹ…‹»³@»*†º)«»¾[˜Ä떼ۺūÀ›¶K¸Ê»¼¼w¼?*¼0K½Ò+±¡‹°ÙK–ØÛ—Ö{³ßÛ½†Ð¼Hð¼K¾‡;¾·€¾«¾ë»½L ¿nɾ¾>K¿Õk¿A‹¿EË¿ú«³Ñ[¾þ‹þ¶ ¾ü»Ü·Œ ,§ÉûÀ |æ{îÛ»|­ìœ ¼¸,‘\‚ » Âòk¹%¬À!¼¤#|») ì|üÂ0¼Á%ÛÂлÂ4Ü1<3¼Ã‚z© ÄA츼ú¾DL¶B<¼I¬ÄFÌ«ÜĬ«Ã­pÁ6 Ä=|ž8œ¾R\“TL§GÅ]¼†W̲?<ÆyûÅ«`ÅjŒÆ`ÅD³ÅHìÆ€ÛÆk{ÆtŒ„elœÇu¼Ä×ëÇlÇŸÆyÏæ ‚<½€,¾bСöFȤ Ç4‡Ç”W¥+šÉ+Ú¢’ü¹÷xð  °¡,ÇbŒ|`ÈŒ¹íþhÈ’_y¬c}ï±ÊP¼ÇœÚŽ)’7pÆd˨ܑ¹ÌÊ‹¬•¼ìp'pá$“Åœ°Ç¼Ë­¹í˜eýgÍÄlÉûº°×̸í¨.o”70ÌU9ͧ<Î"[Άێp?# ðj§ Î\PÍäœÌ§,Ïpp5“_ùvÌëθ,ÎÈ,Ðo¼ÌOÐÐО íÏ[Ðñ ÑMÐMÈРϢëÑ‚)Ñ¬Ñ MÒÜkÒá Ònà~±?3Ý"}Ò,=¿.ýÏ(½? ¢5Y6­ÒYÀÑ%½ÓÝÓP`YˆY)ÃYDgc>üÐþ#ÕJÍÈÆÐìL¥c(йe]¶#©sËYmÍ[½Ð0MÐü·™1¢pÀpqlÔ¼—Ó(¼Ö\M 'ð_­ôI3ìügZŒ×#êÐjÍ׉ÝÖlÐÙ pù…ÑT ä)P+vØZ}Éz=ÄŒ-£L‘ Ùé)3·ƒ}§fNƒÖ8ýÙ£-¬¥ ­mž&RàN² Ú´]Û5{Û_ÛE°›DÐÙwÜKÚL,ÜeJÜ"¥rÔ Õ½ÚI0žxRØãöà`›Êµ«Ø ÝMŸê)Ÿ» np=ÕtmוÌÜ+mßèí¼Ò2 `ÝþÝß!`þÝMПÿ ÊT/WÖrÕ‹ÖçßÃíØ0¡bB‰SU½"2ŒßyÍáÞœ¿ íÜüá<»ßT½¹…N@]ÔNÚ$îÈ&>´(î+2(bÑH.Þà³Íã3®ß®>xÊЛʌØ0þâ3ÞŽð5Gn–äLÞ¥æÝÑ?^ÞAž .ko-ÍJ¤WžÔYäÀ0¯Q-ÃÐóÍUÝ1.âe~¾5.æ… È æo.|qþ¿snæùøM% h—æVè<è\ç( T'ÀmíÌèpþçùÝŽ ´–îç>ÎŽÎÀî|[ŽÐþ€< þà=îê¥î›Â0„6(`Ê­é';æ-ëq|êkp1ç;뢮ëÐm$0ùUìXîàÏîë*éV³ø’-‘јèÈ.ÜíÈ^^q Èá¼í¶=ê,íÓìfЀä­íÀê¥ÞŽ÷,·æ¾ëèNÂê¾î!.è#ÞíµMÉÓžïýï.¬îÿÛÇNï¾ð7=ïÆ^ïÔ¾âŸÃýîïO<ñÍ-ð£ ñOãÏÅÒ.ò _Þ%?Ç'_ñøžòÁ»òäÍ×(ïð÷mós^óó;Ÿó.ï ÏÓ2ÿóÕ©óòÞð=_æFOæ¯~ôÏîfñHïôþOô)õBók½ô½ÞôLïë\¯ÓÐþõ±ö{=ö]öV¿Aÿñ8¯ôkŸ‘#¯ÊZ¿Õf/Úh/ö-õ³‹õnŸôY~÷Ï÷g¿÷ÿþè„÷†ßñTÏóïó|OÆs¯òu¯Ô‚_â‰?ø‹ÿmïødÿô‡Oê™ù›ï{“ó•¿Ó—/ã£Ïú¥Ÿˆ~ïùi_öq’±Ô¼®÷jùJ{úú=ô¼±«/ç^?û ÏøŸÿ÷÷Á ûÀ¿ñÃð­Oü»úé^üºOûÍß¶©?ÑßïÓøÚ_ø×üƯüÉ?èã/ú坸çÏù·ßáoøµïìëýßoÒíŸþý¿ü@€‰Eã™T.™Mç•N©Uë›Õn¹]ï÷*ˆÀeó}[0m÷Ç\ÆfñEž—ÓÉiÿ0Pp°ÐðqJ¬/±Ñ1kMOÒÏoR²ò‘³Óó4Tt²Žô”32Soìn•Õu–¶Öö·v1—M6®õë8X¶9Yy™¹Yh×9:ê·˜òØ‹¸ÚšQºÛû<\ Z¼œZ[ûRû-½ü>^^™|^ú¼Ú+›]ßþ`@ƒê L†¯˜¿qëØµQhbD‰—¤h !°‡Xø¡»vdH‘ÿ,Ž$•ÖÆ0 ª4ùfÌ[%ezB¹Êe•þŽù>Öôùh'šAÝÌ”“Ê΄=‰6uú´ÌP¨°i8‡éBL-³NõúlÅ®aÓ„TË~cɶuëTêÛ¨U¯bå¦nëÚ»rùöõ×ï>ºWÑJQª‘m`Å‹f¼Ò*×½‚ózœüsæxŽ5,ÙRež—;—6ÝŒóé&f5%Ö©Ö²jÙ³º¦„u,Ò¥D/Ý}xðG©…ɧð4Ø£‹7wÞˆ¸óã{l§íø÷síÛÕeç>ݘ÷××SV—rBDzõëÙ·g‚{ü¾Ñ›ƒ‡“Êáòâ“ðÿÀ ?ù ü‰¾âìkÇ<åÈéÁüªº€Â -¼Ãþ ,ˆðÀGJP¸· ­.üžÐÂ=\Q$ƒÑ!@ñ(W[Î7u$ÊEà`´‹DÂld‚Ƴ†ÜI{z¼íGgÄ» AK’Ê—–¤­É#•(²5,QË*ÅüæÊÙ²ôRÂkDóD(÷NŠÊ”íL)õ²3¶8õ4hNÕêœKM#Ù|2Ð.÷<4 >OûÓ7SÄ“9D%•GQÓÆÑ5!ÍqÒNÍ ÓÏÏîÌAo,T7OU§ÒÒ.Å«ÔM£\•ÖhZíìÕ.¸L•T!M­X›@]TÔ<E¹aØ5Ù_ƒ}:e]-6ÒccíuJhµÅHZ\©åÔZ_e}sþÛrg¹U³\)»6ÜlÍ}7t:A‚0À$:Є"ÔÝ‚YêœÝ2SAá=y}j@€ Bˆ€ŽàD!=øŒûvÖvGõØX„EŽvà§>ˆx˜¸«µbSd™æ¢ºý)ˆØ7#öÝÍeÞ`†UÜš&Da™8€ˆØÀŠ@€FX–crA®6æ¡þÚ—›}*À"J@#@ àƒ6°÷ê#û¸ë¢·ì¼-)Ù)Êâì´• !€üÍúQlé&Ú]½×Uìš`zˆ€z t–Ûëuí®»ñÇEç(r™"`g¶4?œ€\Oþ`îÄïîôÅG¿ý‰ØÝJéä|…h`å%7œóÏ·ÝóÐq¾M¾bØáÓ%Fâ†×À4€ûðع^>dòLJ}"é„ ¸Wx @{&0 €€€¬ÅÇÛ|„œÌ“>ÞHz–Bœ¦W¾vî€|Æú4Òð>‘ X%ØÁ%pZÿ£]Gø@åy0‚IcŒW‚Apc. =¨ÂŰРÌ ‡ ÃÒ°† ÇÁ—ˆè³ah°ž¯yÌKâó–8Ä&ꉌ¢§HE!ú…ˆý‘aŒx8Á1©‹¬"¯È«Ùiþ­v\LãèÖ8Ÿ66k\²‹#ç(º:ò%ŒÖ1¢ÐØÇÇýQ.ôÌtªAlˆ|‹"¿TÈ#:’z“¤[(™¦G¶ð“™¬Ù&ÛÒÉ61Rƒâ(ýøE;ŠŽ$Œ¥ 1ÉÊ£‘’,¦$T(‹hI[ŽÒ•€¼ã̲XÂ-òñ—À,#ftéH_^—É|.ÃÒLõ¡r™>œ¡4#ÌD“ƒoÔ£,ÇÉMe&ÉšbÁfhÎ[zs’àÜ!;(ÎvºóœHJ'ÁÖYÌYâaÔË>q£J7îQ‹…—@¿BP$h“Œô\¥BEÆP¯8T Å£?ËIÑi““òü!GïIÎ’zôþ£Ë| F±ÖO{Ö¥ i)EºM„`%fLeªÒÖ4¢$…©I‡ÊÓmYt*,=œKoúO£> ©PQj £)Èg>uUQ}ÊT{¨Óp6µ£X–VáT4¾t¢DM«XƒEÖ¦pÕŒU]¤\Ùª'·òȬ¼&]=yÕºJê®A+ zV°žô¯Yi.óšJ¦ªõ ‰¥U`2ØÂêU|•,•(‹ ÆfÓ«óêZ7Û©Îþå³,-¥XZD¶&– íHѺZ×v±ÕLíhm›ÓÇê =îîpE ±_ÂV&²ýí1:&1 ºÑÍ­¹åjv—~Õç„2Ô] mȧþÝ /aÖK¶–µÓe§[®êÂäºÚ#v ÄÞñ¾ó¾l,/b}Kß½ÊWGö•æ{­´ÛÚnôÀ;=”€“I`“ÄW½Ð0‹|Üö^ÔÀ‡-*sqêÜÙÚ”•þP†!‹` “N¶¥ˆ[DbÖÊñ¼0^jáü¾Ò¿™p%#ÌY6ÄNª‹9ìÔ#sÆË…$‹CaôJ¸Çèü1fÓÇd8YÆP~²•lH+_ËHÖr–¹œã* YªDîïŽûåív¹_–“šÜÜônYÊpž£œ'f;YÌefsù,?wøÎdγ™•ˆæ­Ò9ÑÎá”ýM=§±Ðþ9´‘#ýgA»9‰™†È¦ÍÛiDWIÅA¾±0÷»á5ƒÚª°¦0¥W=2Qó Ò€öô¤/½èAKðÖ!5ë|êOãyŠÁnL®AùkgÊzE©¥²Ål^3ú¡´Hp‰Ûmõ÷ÒÎ$µ2lW›Óáx®tÙýŸZ_ÛÙ$7I¬ÝKh·ÙÝXƒwù]!ðÒBÜK®ô[ë=ßxëøÞÌx´ÝàãµÕ(Fw©uml25 óÃ[ðXçûãŠö†Æ…Õëds¼²ŸëÁù‰í#pÛÛÝw‰|Œ¯åžxo'NlS§{%í:hMî’c:ç¨Ý¹‰iþÕ¦‹¶Qèþw¿ÿÍ[¦âè]œ·’TÎc¯<áÙ û³¿Žˆ¬ŸüÝ!]zÍ_,i{—ß"7øØ©Rt n}æ–øÏ)Þlº#î†8{¨“Û®ÇÝís¼ØOv¹GËî4Ä;¥J—·ôòKÍ/ùÐ[wôŽO|ÈgoúÒ{ýö‡h= '¿™Ø£þô€Ï½åWŸÑÈ›ý檖òÚÞö]¿}ø¤¯=ûäOûõðý½ð«OüÔôøˆ¾Ñ¯?îì˜ùP?1ÏùîóŠÞßg}ùžvš¦Ÿ¶ë¿z‘ûÿà«^þü`÷‚Èþ ÿ@Ìù,þúºOúÆöÐfÂÐÎïÁ¶ÿü6¯«$°ü80…(pİÆôíøÏýü³ ëNI[Œ,áo!úpPü<ò^ÐqzôŽýÞ¯ÿnm‘†þ¼,›Ì3°ø0/ 5o 9/Ëb 7®uk© ¯ ÁÐ WÐø|ЛO6`.æ†kæ°® ¯ì YÐÐûÈP ÅPµö/T„®Ý¶P9ü°¡â° «pç0 ‰0Sá½Eê¦Î»ª.ãÞÌñí0 -q £OötPð1]¿3<;`<ÕÓ>ï“=ÕÃ=ã“?·s>×îS@Ñ3?Ócþ?û“?ÿS=Ès@´@Eà@>4=´AôAEà.t@4B%Ô;)T,”CÕ3CI´D ´=@ÀE_FcTF@ÄS=4EM”=P4GËÓCà=AT>m´B{4=OÔHUT?£²4$u†*¥R&«r®²~†`_ŒSK·”K»ÔK¿LÃTLÇ”LËÔLÏMÓTM×”MÛÔMßL‘ò1brxЧj²4ò-‹À:cNPUP•P ÕPQUQ•QÕQR#UR'•R+UPC ú¥4¨ æzN§~4’#=$oËTÓ§}Þçâg~®4 ßþ‡ OUVg•VkÕVoW£}ü±21c2 „1Ú‡a€ü¥òçú§/„Uˆ•æ@¨”/@ðg€DàX GYåÃ"=µ3@mþ4Ú/H@b2€‡ =`#5U.ÐU]€]‡àt.&=â†/Æ ìÒiÞ5^åƒNQ`&5C\{53LÀ^‰à@cJ &@1 ö^…žTÌ•,äN ]9–b…€p06>œJ™43&Óa1Cce“6§4c#v:Ö4¨rdkvx¤u;ž³r¢33Äõ} ¤”1fe‰V& @6dq–cõ@þD60fÓpHÀií'jãã5‡bcs¥& m8ÀÀ.ƒiQfeÓJãmQ`4Öæ^¤Ö- È n«+¹Ã6UÃ'—–jWæÖ/ ¶X‘*1'0(€h®Rkcr ÓrMC€oßâm½6t¡¶t¿¢nscÓrR7l¹ƒe… JeãtV·-Þv6#Ófé–j3²p#†|Va €wg#aUF6wq£n'¶b/öf7Ö*×/Žwr%ödQ€p¾—;†:Õ65(ÀÜfmÛV1>À(<ÀàÃ]A^ sÛB~é×~O þþ>j™·/®[1&BÒ]?àV>èå}þ±34@`¯öšó @@êG[‘µ[ùƒ¤Là À( yŸ¢¤‡¶5YsÕ†o‡sX‡w˜‡{؇ˆƒXˆ‡˜ˆ‹Øˆ‰“X‰—˜‰›l:à ¼VN >À‰· L cJ€ƒ?Vú÷1#€‹ÓXâö…‰ ‹àÛ8^Wë8 æ?s‡àZ#ø^ˆ2@(€b®Ôù‹¡cGàXß v¾×k5 Tø¼–„sGÀo 45@ ׎KÙH &5@Fþàx‡ À:rŠ ~>@e°ò®2äW•µc o ’a`@`t×k º‡ÎæzŽ×2À{ +‘WÙ”³™–“©Xì%TxýV^×¶,xGVèx~ˆÒw´XN§¼h­¸›cÓI‡€šµ™Ÿ—›Q •… öTàoærŽW{ð>à6g~ÀMkSb¼Ve›ö™ [kRS÷¹Ÿ?šwš,`O š… &µç*eX~<ögÛ5:+Y¦70W^ Y<¤{º@ „g¤QàZ‰H¹¨Z£í> =â¦Q`¢‡@\þ%&pd:£szpyÚ§»šþy¨“º I™œ?´‡¦@gº©ñ+¯z4¹›åu_꡽¯C!‚ZsLZœ_lÙƒëç*% {<`îõv‡GˆÙ˜™p0’@«ëÀ©ù5¯C»ÀÚ›ÿº¤InY&à*ë¶u:íò±¥’%™’-£û6{°P[´ƒ»xv ˆ[¸Û $ #=`ŠmZ ¹£[ &@…`C«À¸¥[»·›»»Û»¿¼Ã[¼Ç›¼ËۼϽÓ[½×›½Û۽߾ã[¾ç›¾ëÛ¾ï¿ó[¿÷›¿ûÛ ¿ÿÀ<"‚!ùd, X… 1"""+++444<<<Mci|CCCLLLTTT]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@”pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Hˆ D'þÏž;BâÓRÑ4 9ê†)΋  ÀKØÊU©ÁAG™-4ŠS4"  j`B‰&[h%‘äm™–¶5óÖïÓ‰e#Pp€¾IÈ@™²†! €@!B qÓÑXM~2ºtš 8˜0¡qLòÑêÚˆá1€ÿ4Ãv8Pã‡)ž(BÀÐÈ‘€ø°ü«VÜ^ªù]üOó#ˆxøb‰î"ô.Ò=MðãhÚ OÞQJ Dw¢€."<0Õ¤†ÙXHpÂ{(€E à•j?©åÜq#8@ þá @L9!ú ¢ˆ „gBSÐ!:^™`z©××N#<°áŒCt€Yh …%„€àˆ ‚ ¾ ˆ º¦+pvà‚^5ÇÕVPØåQÒWQ\Ùh ‘Iô7A ˆÙYGp< ™kh&Á áf€Þ~k V)4ÐÙg`êtäD B‰HJƒlaŠ@¡ŽÐ¨ĶÞÍ=WÄ‚àiDlG¤7ë© €À¦¤)l N` hÇV€ YD&šl£¨®–Y„:@i:A»ê´¹aí¢þØFB¢PFAxÏ ëgºº¹L€€Kîv&”‡Ä cÕ é~vÐì:àšƒ¡¡À©°ï~‘F¸_{þÄ{ aŸÕéô ‡ƒe|ê„&ˉBü'Dþ!Ñ€«ËGl•?MÁg¿ò"Pµ\bè@œ‚•:Ä÷ 1±_<_OO> šÑ¦6XµÄ`=„Ö\Û 6¤ÌÚûV[q¸D ˜p‰ °á§Ä¤æÁÂGÀÖæ†.A±DL¼ßÄa Qe[LqìØ’‹sÔK2ù¤‚àllKÐËŒlÈñï¿­þÉDÄšfæDŒ°`ûI.€‹kµf[£èáB$Ž[½£ ðŒá8°«©Öüón?d‚Ë ßlÑŸÙKx€À æ÷KDc´ô¾{;'ì.„ÊC¤…[‡[„ !c‹ßЇ?ÉmìkC¨ß"¤£#¤ ÍùÉÏ‚Všó ÈGHxÖÿjÖÈ A+%CàÙx„‚ey'¥³¯ö -ðSMø„°/!4¦7)dYõt2C){)U  “ßá¨l²+‚p>%,ëu[)Ozú%€ÇIíT¬¥(w!"8H‹j[Þ`>‡EÁ„±m(šPn¦¹#ìLÿ˜þ?€‡Eø" ¦X„}Aæ-7²¨˜ÅüA1Šü#B KBo#bDnÈ^a|~Z àõ­¯‘Iè ýìG¶ A…Ô{NÇ€r:"Ð,d…ãɈ€JH±•l­D¡r%aŽ’ €Q†K!ê?ò³#m‰‚@¢@” SñãI!ȇ†$„æHKäüP’Q5à  ¡?{ÂÖN&áxI^ãH©;ãèD‡ZN¥°¤Xµñ-òÑÚtRH" 1ºŠŒ (s"˜{ÄàØB»!؋˙õ¨çMÊSzÜ û·•¡ iü´æsU,ªDÁA ˜„怨(öKör˜Q³*V—F±¢Êe+%áÕ”ÒáÕ'4*3¿¹×¢ÞŒ©cMäœÙR‚hÀtUzàAÞ !²Y”îSšÌ]?ó^Bp€Þü´šøÌl$à€ª˜KQ0€£z§I ȖǶ¨EqM€\Úªqj+àöQÌ"¤F#Àæ>´± ˜ÚÚ+‰ú†Ye¡p‡€¬E-‹pŠÅæk“%þ[äŠÑ8©….k]^ºuqä­"´ðäµ±\qÅô€½!Á‘ܤ·À1ÈÀ’|%=ŒÅ1 ðž™^WÌ"”ÖvJ 0•ªHO˜Ò‚š§¹ `'Ð_"WI¡ OÝŸ‰)0â°&VÅHˆË\žd¼¸õTÖ½ãNH ¡²èÊ|HRÊÃX„XLT’jâÒa|˜™MÖÀ”Là%µMím2~¯ð³„`‰0ÁÐ<­&aÆé :o§‹cnƒŸºZa!dÆ¿%\ Jä¼»%ðiÓÍ3àÐÅ:´?IôHR>2…J+@Uô¸ÜCþÉÍjª Ž7âæ©x‰gš~ôM™ /Ñ©Ž5Lš£QZgb°–µ®UÒTŸêÓ¾‚ :ûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØÎ¶¶·Íín{ûÛà·¸ÇMîr›[Û!è@&£§ÈÑ— ²gIïzÛûÞøÎ·¾÷Íï~ûû߸ÀNð‚üàO¸ÂÎð†;œß¢…œLI?ÌÀ¿[îh‹uîŽ{üãËFÈGNrs‹¼ä(O9·O®ò–»<Ú4ƒ34úã€b‡ ØÀ› ÀæIæW½x62] ¢SÃèÒ°«¡q³×AÏ5~ñ”>tmþ ]W‡Õá±õkd_FØ›Ñuw”½èVO;6ÎζO#âÔ€ûÛ³ávuÔ}×xÄÝѱ÷¼û}7GàÿNx; ž‡/¼âáxq4~ñWÃãÁ1ùÈ[ž •÷Fæ/Ïy/lžŸï¼è±zm”~ô¨—Âé×.ôÔ»¾ «¿Fì_Oû©·þ³¯½îEv{³÷~÷À·Bî©1ü໾øIÿ½ñ—ÿäGÃùÌç<ôŸ1ýèG¾údW¾õ·oì3ÃûÜÿ;ø•1þðã½üÈ@¿ùe­~c´ýš~?1äÿ1Ó_÷¯Kó þëˆþç øö2€¼`€˜¨ ˜þ€8Ñ€¸Xh 82q´ ø( Ø,‚°@‚"˜&è )x‚&±‚¬à‚,80¨ 3ƒ Qƒ¨€ƒ6XÚgw=¸ƒ¢§ƒ¦ „@˜DH GX„‘„¢À„J8N Qø„1…ž`…TØXÈ [˜… Ñ…š†^xbˆ e8†q†– †hlH o؆þ‡’@‡r¸v yx‡ø°‡Žà‡|X€ÈƒˆòPˆŠ€ˆ†ˆ{?Èw¸ˆ±¦ˆˆ ‰¸”h—X‰Ž¸tm÷ˆš˜g™H¡ø‰ˆç‰å0Фèx¦XŠœ˜Šñ·Šã€Š®ø ²µþ8‹ ‹ªØŠ¸ˆ_·è¿Ø‹¬Ç‹éŒÂh ƸÉxŒÓ°ŒyàŒÌ¨uºÐÎP†7ÖhØXݸÉðs Žàè~ÚH‹çXŽ1AŽqÀŽêˆéØ îøŽý¹HŒôø€ö¸ ó˜½ÐmþÈ€ûhz9-xÈn"kà Y ™‘ ¨²‡‘ùy¹‘*¨‘Õð‘ ù‚"I|'Y’F˜’ɇ*9‚,ù|1ù’A’˜7“4)69;™“Hˆ“×”>é=E9”R(”ß§”Hù…LI~OÙ”d•éG•R™†ViŽ.y•"q”_à•þ\™ `Ùc–k˜•ÅP–f ‡h9m¹–xø–ð¸•pÉj©wY——¤'—zi|yù—‰è—õH—„yƒY‹™˜˜h˜£pQq`WóƙԘŽ)Š) $ ,‹ãnD ( Å&ý¢™›©wI õ#šC (ø¨š«i‹­9 &pEG@3³y›µYŒ¾ ¹Y™£©ŽñhÅ{ˆù›Uœ ›°y?ð!°S‘œ´Éœ€çœŸN@ÐCÙ©zp—Þé­5eÜIžÔ¸žÀeÄ™éIöpŸ wì‰턟1t çÉà)žî¹ŸòX ™ðþ (à 8Ôà  שžËi  Q–éñ/w±8¥¡,d±Ã4žJ&Ú)z¢íˆ ³°¢,ú0ª3£lP£9æ¢6Š’Úž=º£n¨£±€£@ê‘BZ‚GZ¤Aù£èȤJÊDÚ}Iú¤Ë¥J¥Z8¥­`¥Xú•Zj’NÚ¥÷À¥Hó¥bê–azgº¡fJƒmº¦‡d §}™¦üø¦túxz sš§Œ¹§C¨~‚ú“Àmv6¨M*§g§ŠÚ’ŒZ0©”Z©–z©`…ú¨a¸©M€¢:ª¤Zª¦Šੜz }j¡zª°jª©þꨫº¤Œúª±š«¨ªªµÊ–´Êz¸ª«°:«½ÊŠ·*¬¹J¬Åº‹ÇЬÃʫ˪‡Ðz… ê¬Ïú«Ñz ­Z­Ö*«Óš­„ø­\È­ÝJªÊ ®Š­ÈH®å*ªçŠ®wª®#É®íú®ðJw⺠®Ú®æš¯÷ZÛ¬õê¯ÿ:˯£j¯;¯òŠ’[® »°ÍH°fH¯K± Œ{–Û­›±ÒذÛ±Öú± k«\g±»±'ûŒ,Ë–$ë¬&Û²Uú²“°¯»«"K³h³u¨²%ë³<+›³3;´Ú*´Ò³Èz´H›–J»—@+³Qû´7Zµ€³ë´þV;—ͺµXÛµp¶‹ µüʵbû E ¶;›¶«°¶gK¶nË“r›f;°m;·9X·“8µMË·zëy€û˜L+¬h¸¹·x‹¸Zùµq›·Œ; Š{±¹¡0¹+[¹–û ˜´š»¹Ð¹Tû¹ «¯ƒ °~k¸§[ºS ºKº¬[±°Ž©««‡»¯àºª;»¸ë«Ž»¸½» ºk»«¼*Z¼¬Y¸ÄË»Æ+­ÌkŽÊ›¬ÈÛ¼I0¼Òû¼Ô®Øë–Ñ«·›½|:½¶Ù½× ¾(¾p·”k¾‰¾K¾§ú½ì+¹îÛ꛹óû¢õ»ðë­Û›¿œù¿ðØ¿¥*þ¿Ì¹û«Œµ{½Œ¤ ÷ë¹ œ» \ž ì½L½Ö‹Á<Áö›ÁxÁ£ëÁ[ Âw ¯KÂ`ú»ë«ÂnÚÁj{Áåë©°Á3LÃá Ã(ÃñkÂÁkÃ=¬Ã8Œ¢>ì<ì¿CœÃ,Œ¿I\ @ŒÄMü“B|€G\ÀEŒ»OlÅSÅnÅýºÅ\|µ`œ¸UüÅaü©cŒ'°Ð|ápe”eœ°W»ähËr`{®{sì®u̺ä#{#tÐhtKÀf|ÆI™Æ† m$t c«ÈtìÈŒìǘLi†BB÷”l¤– È›œÉb@ŽíG{“ˆ|Êþ¬³¦L­¥,˜E0" ¾æÊ£ ˱<®³,7s`6vòÊüËvÈ3 ðú)ǽœÌÊ|^|ÉÕÜ©Á<¤ÈÌÌ KŽ€ú‘ %Í9ëËÙ̪ÀÎÐZF‰|ÎÔœÎëÍD`_Â’ -ºyQgdR:Íøl¹Ù­|G¦“(ѹslB¢+(¼»ö¬ÎÛl“üÙVT qeJÐy{Þ–¨ÍyÐýÏkôhÖ5ÑËëÊwÑ¥KŽ € °Ó˜=M°ÑH À†'2ÍÀÒŒ©H©šjÓßÈ:PÝtJ F3þó4]EÍÁ®|ô\Ðäiž.mP}<1Þ³Õ7ì¥^m´`­bÝÐ0ÔôÙÍykkͶ'-˜E|s| psMÕÿñ)ð66׌ý[Ï( wý¸yýyŸÿ¹ ÷¦ÑczãÑ Í{"ÝÕ_ÍÔ›Û Ú~½Ú©ý×M   ê úT<×f?§ž£-Ç“ ¼•ݺm z&*Q-#]Ú¤ÍÖ¦mп-ytÜxÝÛ¾½Ü{`P\ÀέÃËÈ­ÛÝ-ÝQ@Ž% "Æéå hÄÐMÙàÞ çsqk<É9Ïʽ޼ÝÞN@Ž€­G}ÆþÝ÷íÝ®ßûÈ#"V3’Ñ¢üÝjíà¾àŒ '`DfÞPŒÞ’ á^½ï ! B]É~Ì»ÝÂ.á,¸U[Ñ‘F–ÏMàÑÝâ4Èhuëv“¹Íá+ÎÄ:âÔpðã~ãBžâE^¦GŽ`€< ^à®åQ~¥Á0•¦Çä[žã]îåÂp>÷(Îå6>äœæR:å|@p`oŽæ‚+ç#Lçjþ k¼/Üj^jæ}å]NŽ @.JnÒ6Žèq®èQNŽŽ1õå|>épNçäH2êägÎÞ€.åK<çOÞéiþ~ͤŒß,^êAgçá+é*NéEÎêè<êù]ê¸Ù.îç) ëÊyêžê›îéÍM‘¢žèª¾èɾ礮ë¯ÎëÏ®éÑÎìǾêÕÞä´¾vmë:Þë3ùíÍ^éÛîÇÝŽ—^î·~î@î·îÙî첨éžä>ïæ^ïN¼ìœ®ïíÎï?yï} ì-ì±NìÁnì×èâ^×òÞðÈ.ðôKðzmð3ðïêDëã¯íÿ©ïoÔÂþñ ¿ëïîö ðï®òÒÞñÔ>ò—ëïµÎîáîòó&/óNóØòô®ð¿òÓÞò8Ÿ”%ÿ§'ÏÕïóþæÌóAoõ3Oõe.ô¾õi}óHŸñJoóLö(_ó¨îñZè\¯zÏòoöR?öiö)ûôTïF¿ïrÿõÿ¾÷ß÷ê òp/ò‚¿áh_ìjßôŒ÷]ßöíMô_øG÷zOù|oùXŸ÷o¿ôqÏù@õ—/Ý’¿ó£?ôk/¸Žïö^?ø)¿ú‘^úþúˆo÷ŒØúîmûZŒð /ú€óoà§ïíŸOö¡Ÿû´¼¼¿È±ÏøÔªû˜üu_öÀ¯ù¿üy]üêÞüØ<õк:oüÞßêàø½Oø oø×¿þݯý'ÍýøNýŠû¾'ýÍGÿ ¿øèïþü‰¿ÿ@€‰Eã™T.™Mç•N©Uë›Õn¹]ï×+ˆÀeó}[0m÷Ç\ÆfñEž—ÓÉiÿ0Pp°ÐðqJ¬/±Ñ1kMOÒÏoR²ò‘³Óó4Tt²Žô”32Soìn•Õu–¶Öö·v1—M6®õë8X¶9Yy™¹Yh×9:ê·˜ò8ì²NXºÛû<¼”Q\œZ›»‹X»-½ü>^>z¾û¼Ú}k]ßþ`@‚ê T†¯˜¿R˜ØÍ¹fbD‰Ÿ¤x !0…Xø¡{xdH‘ö,Ž<•ÖÆ+ó}ÜrBDL™3iÖ¤yÂdNþ;¥”äù å*•VX&t©E ¥K™6uÊ”ÜO©Suú¤ê(h¦¡UŠj·¿í¤óоü ¬¿×»L8÷ øÏ°¹ð. ¼°¶þ1Do¹ðš£04 ƒÀ MD0@ûö3FÄÖLQ?ŸkñÄáÒp÷1 »“¯7…D G u|ÃÁŠdÆ—ŒpÈ(G+òÂ#­iRI+’9)½ŒJ­l‡G$|l¯L5žüñË6oL“­$ <@¢§L(bÌá>Ô2D.=t“PªÂŒ¬6!:8‚8!„˜p"¢Ï$8“?,7]MO  CûÀÑ!Ò¯2…“OP;4>RmÍÉTÀ"€ˆâ,p!(Ì¡ iØÃ±pz#œ] y¸>ú‰(°à …H>"Âð‰2Lâ—¨~çˆy‰`ö¦ØÅ{‘~‘óÙýþEFÉy±‹U¼Íµ˜EÄlþÑhD£mÃÆ7ºqBpô“©hC+6ñ}1$¡‹ÈÇÒ1C€ÄŸ ‡HH(ò~\£"ËÈÁAž°ü "‰DÉ32Ò‰Ž”¢&=ÈÉÒØ1x¬K÷HÊH‚ñ‚büœ%‰ÉGº’‚¦$ *W©GMÕЗ’ÄeÆt9%OŽ‘–¡´å(‡¹¿bV†—#R¥43ÙÌø=S2ÑtQ5±ÈMk–›‘Ñæ{¦¹Í[~~á„Ì8L òk€èÌ¥0)Ë‚2Q¼¤<ÁIÏNÚÓvfD&>ÉÏtúó”Çœ%A+ÉÐOÔ|ê ;³TNrz¢¸“(˜zO}Ö’į̂3ºËŽ4þ™ùå>GZ¼þ…¢ŸéøTÚRã½Ô/1¦EÛÉS›Æ §o¨øºÐ*ó§· j[tZ,ŸÊô©I ÚRÙÒÔ}±ò—NŨTGÕµXµbQÕê9¹*:¯^¬Á›iQ=ZVÖÕP'%jJ ºRºspJZÍ„UâÁS‚xuiI9Ôöѵ¡G­©`»JXhÊõ°¨bëÊØÆÂ’‰†'[Q*Y£Zö²¤â«šÖŠØ‡‚–qz•ÊhTZ϶µ@ul6!»YÓNÖ®H­ÐTûÖBP¬Wuíne‹Ù?j6°4­lHƒK\Bõ–'¿½×ps»Xç² º;‘îËEÎÎõºþYËnUj›ÜïF–²‰ oËÆ‹«òz÷¶Ÿ]&K× °öšd»è¡î|ïZßrÝw$ùU¢_ßI`ÿŠw¶â|o•«^þêöÀ˰HÜ]Ÿ×¶ÆØ„™µàV¾¶³é=­†ÉÅaTØÀ ñ²`b¿X—⧉/‚âý2w«…JÊSxÜcãF4ÁëôpV…Û\µùDk8Ë’Ï’–÷óÉõD2i§ÜÚ*)Å3òD‡ü×,Ƕþ2:iL_yºhÓ˜¿Yfsu¹À7Þ!Yoeá·yËE.|A ^ã¶Äl¶¦›%ræS“ζ²3‘{Hèˆ:Ñw<´›g³Z:§pþ&`ˆ«»\eU:ÊÚs4D ÍÌnFº -†ñªe"ã7ÉÙ•£˜¦U h£z•=ÖµSB½5A7SÖ)5}Û8éè„…ÉÉ‹“9 kR[ Ãîo±qhSûö×Äv@¤ aj_[ÒÕÆo¶q¹míùÂ`æôƒ­{jpk—ܱƴPÕ¬ßzØÙá~7yó Is»…Ö6¯ºýÜiǵ߆ü7INpô|ÅsÞ÷TÍêU»Úuñ~ö¼™Úð>?œÝžv7±³k“+¥×ú&¹¿9^UÿY¾·ž¸9g~le+›Ù³5D>n·ûÛ+ïå½w®sk²ç.{9Äm-q¡[ûé4þ:"Ю唖èGO¥±{ÊõŠzý8ZWxË¿ºôÜéÓV¹Ú·.nª‹IÇÏEt¶ì;Íû ª.O¹ÃƒîhùÝ¡Nx©þí ;ÙÑjv™O½ën|Í'ù¸À]ŽGà#>xo¯ýóm§üåŸyÆÇÝwfzÌÓz¼Kþë°{éçxú½:¾õ@½îEoy¨î ˜¯ýÕ›zFøaM>òe¯÷æó]ø^Ô|`pïyÞ¿~ô¿~‘—ÿ‡¾“Ùö«­¾Ý]_øòþüÝþùh/ýðcÛø^nì³ïüúõþÁŸ‰OÛøÇ9ëöÏþ|oï¾ÏúOþÁþoÓ îì:ü®Ïü${5*Îâ^ ãrçý¢küv¯î@Pð"0{ÏIïÞÞääLN££¹Œkõ¬¯±ñïõ/;ÙnnÉr¦Ï>ÐÓ¯òNPûòOùÖð>¢ïò^PÏ »Ê¬ÐʰР°Ž¸„0ä„Nì$ L`W@@MbPàø æÂ,÷jpå°wÐå,0ø¤P ¼†QRÅNÀ @À@ ×F Ó,¹Ë q ÐQpaªóP;–fœæ6À gñ­ õ'l°éhp¯ ï•a¢ÏGþ"fˆÀàWŠ  €4`o‘ û —r ÆgSÑ Œ‘#‘˜‡± ;à?&B†²…På‘ .ÃÞ¥ !PÑqÉÑßÁM#§qÿ!]Ö%aŒ $€Bò†CFQ ™¯Ùäš‘¥àÓñ;3Ð&\-!Á C ?¡`ìQa†àòqûRrq"Ò[‘û‚ Hr•+V‰@%ë0gíáY¢eZŒ`¤F¼±øq,@(‡’(‹Ò(- e&@ šÒ)Ÿ*£Ò)A`&ò¤Ž+2);`)þ¥Ò+¿’*eÂ*³’,‡r+g¾R-¡2,cb,Ë’,ÏR&˜r-ë²-Eà-á+å2&è².Õò.Eàþr-ï2/õÒ(ùRü’0¥20³1Ù²*@À2/33S3@”R&Ò22“& 34›Ò0à*S+=³/K3*Ó5%S,qQ;$@f†às&yq|‘o† O\R8‡“8‹Ó89“S9—“9›Ó9Ÿ:£S:§“:«Ó:¯9_Ñ80±iž¦81=ÑL *Å!Ï=ÓS=ד=ÛÓ=ß>ãS>ç“>ëÓ>ï?óS?÷“?Ó3:`O´Ãk@ QÂ&þfø ` Q7IÌAáçì& o à…À ànÒðA;ÔC?DCTDGt-"t ý19ø±)jR"4Q€ø¤ç§6\T`”,”)x5@þf€D`FÆF‡„ ô:@ aÌ365H@ Q2€& = ´4¨ÔJK‡ f(%&ì5ÅÀ…`иt¼´F¸4Ñ:œE«ÃÄ”@.…]&@7þtL…n“8ìôªTQUÆP…¤q“6«ƒû49U#92}´6àºÔ6ÂvlËöJ`MAàzgkÉ4*%µTmÙvHäänÈð:4ÀM`'Y´ À)øÆHi4IQƒq›¢Là À( gý¢ž‚»Ž´FI”tK×tOuSWuW—u[×u_vcWvg—vk×vowsWww:àoà tU; >€w“7L  RJ@qSUö ”{»Á` s£ {…à{a6{Ç—e)€`‡`H?ÀM `CÀM€"å7=§aQG`F? ~ÇþiuU2s?@W9@vÔu" @›B^É—‚q8QD`fv `Ü‚7ãÔN>`U~ñ|1Ôö~íeQ €\Õ0Q`@@`UuuàlHaÂff ÐÆzŸ¡yÏ´‚“Øøt…WèAàz…6¼[ ‡]V5UGQýPy… fGWE•x›8#msŠX‰Û˜˜8X à¿Ø`¸qf–l8±pBP»ØÕî·#EW6Wýpf“””ÝØ‘G¡]ÓwŽ™&¸‘Q€ÉÆAWlõf³4' ”ÖKé·f§ø‘Wþù@ ˜F’Q`H‰®Q˜1‘1> &Õ“Q@‡ÀIa¦õŒãNùB/™•›¹à8–gy*YÔ5@1ÈF”ÀTiy—Éô‹y”›ØKó„oðØ™ÓBà•…@ŽQ §ùIØL˜qùÆ%àl<`ÆTd›Fh؆qxRïñ`ü`e •yxMÕ¢šž)y‚Q îW&ÀeìŽt'ñŸ€ãw€ º 8™‡`¡Gà~à¢#Z¦Oe¥ ¦g§ßAÑ‚—”¥àsZ¨Ëa0wB‹× nz¨—š©›Ú©Ÿª£Zª§šª/«Úª¯«³Z«·š«»Ú«¿¬ÃZ¬Çš¬ËڬϭÓZ­×š­ÛÚ­ß®ãZÂ!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹•••¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›³Dx0D'þO>{p %(%£i‚ÔMSœ#,@€ $–p@@µ@ƒF4à@†@H¦ÝY”í¨Z0¢ Gä©¥LÝžyêêÄH€p€¾H8VÜ € DLŽ``( "²þ Z4PÁO@×U@"4ÀÉݼV)LðéŸgF þmc‰"<Ñ)Àô†p`@t‰j{9N†·pà|’A„ƒe°Jn)»ïð¿K‰£ù^„wñŠ 0@’¿A²ChvÖù-wü5@Õè&DÀ D'„HPÄþ GÔh Žd!à>‰Ð€|`†lháo*²¸y$<Àâ'&¡Sä‘uK¨·a{Ëí$,šhÄe~WcY¡„”àÛS¬(áºù4‚˜d)d„ ziarwʼnYŠhªyE€^67†þ ±všP‚k±€iCh0à¸æ@Ø  ÐAAøfÂs1Q@|ký„”p°€X¦ÝiQÁ õœ†¢Æ«PêXœF Y"§œzõˆ„kG°W¬P ƪe˜ уœ°w:-À+X¦¬ ˜bËé—þ¨EÀ¤ @·Z·È.ãJfn§ñ}€©\ yÂy‹¸âÞ D€]&ñAPÀ¢ð¨ ùíu„0ºÁ¶î  Bh䩟ZÒ4Ê Ð×"Bh E8„.q„´ „H¸L( OØ„(…°@…RXVè Yx…"±…¬à…\ø`¨ c†Q†¨€†f˜jh m¸†ñ†¤ ‡p8t( wX‡‘‡ À‡zØ~è ø‡ 1ˆœ`ˆ„xˆ¨ ‹˜ˆш˜‰Ž’h •8‰þp‰” ‰˜¸œ( ŸØ‰øŠ@Š¢X¦è©xŠò°ŠŒàЬø°¨³‹Úþ·~à‡‹¶8lµhy ¸‹GÕ‹‡ ŒÀHz¿{ÇXŒ+˜Œ¶ÇŒÊƒÎÈ{ÑøŒ58ÂgÔ¨ƒØˆ|Û˜?ØÎŽÞH„âh Ä8ŽÒ¨‹æ§Žèø7çHïØŽÜÈŽÆHòXñ(ùxæXŽñçü8…iXXûgiû ¹ˆ©9ÉyY‘q‘}À‘IY€ù‘/á‘{`’$é!©€#™’,’y“.‰‚+ù€59“!“w “8)ƒ7 ’-Ù“'Á“u@”BɃ?™ Fy”A˜”è”L¹K‰zP• 1•q€•V …UY Z¹•þUØ•#(–`9_égY–_H–4”jÙiézlù–™8—-h—té‰xé“n™—l¸—½—~Y ‚©…9˜x˜7¨˜ˆ9‡‰Ù˜‚ȘHÙ—’i‡”™ ‘y™›°™eà™œ‰™Ù”–ššc€š¦¹‰£¹ %  sEÃF§š« Š­© "À+’p3¤ ðm @¶y›¥˜›œp(¾NêXœÆ©ŠÈ¹ $À7XT~Îùœ¯š0²©UÐá VX£Øù ×ÙmÔ¹1ÐT!žçYž¾XšÂ0˹·Fñ)Ÿ†°Ÿ&`ŸOÀ$EàŸü9þikÝyþªŸ€ ð  àxŠ¡Gt Ú"Ÿ6CŸ*ñÙ) ÀÀ9H²À€ï9 äù¡á衾À¿S’±œA°ÖH*£I£3 ¤q£a)¤Dª—HÚKš¤£h¤¯ð£N“Pª…U:¥"RŠ¥v°¥Uà¥\:`:c¦pP¦Q€¦fÚjúmº¦†y¥kÙ¤p*‹rº oZ§g§LÀ§zJ~ªú§a0¨êG¨a¨mw§ˆz—tJڨ砨F@©’š–Ê ‘z©ªÇ¨©©œú¥žš†£ª¤©¥¥jªF˜ª„ɪª:  :ž›þúªéˆª³J«×x«YŠ«ü«\Ç«½êª£à«À*¨Âš˜ºZ¬Lj«Êš¾J¬ÍZ©Ç ЭšÊ¬ÖŠŠÓÚ‡Ûš­ÃÚ­“™¬Þú Ï ®ãú å*®çj“ê ”ëŠ­vÚ®ïú”òú”8WoŒ6¯,‰ª W¯úЧæzˆ0P°{°›°@û¯AŠª`;±[±kаËå±û±›±þº±§Ð± {²+²$›‹²(«²+K®› ùá±.û±0³û*y6{³!;³:[”@{ 5ë³8;´AK•#ë•гF›²H›´gµ¬é´O‹±T+µlšµþ¸iµO›³Z{ &{µ ¶aË´K;–^k´f{¶Ã0¶d+±më¶Á·q;·tû vK¶x›·Éµ‘P´q+·€ë·©Y¸ ¸ƒÛ·†[™-;¸X›¶Ëš’K“k볌;¹·°·W›¹š[ œûµˆû¹\ºl;º¤‹}•[·M ¹‘›º»ºwy¹7ë¹°{¤»¸¨{»V`º˜»»¼ë}²«·­ ¹¶¼sš»w ¼È›¦Ì›Š»¼ÃÛ¼ðø¼¾H».{¼ÔKªÓû·Øû²Ö»½K໵¾â›䛽æ{¾‹Ú½‹ù½'«½ìû­î» ÑË·ë;¿×ʳ®+¿úË­õ‹”ð ²þûþ¿áª¼øÀÜ¥ùk Å«» ¼Àt¾àÁ¬´ܹ ü¿¿¬¿LÀ<¿!|´|ÁS{ ûÀÒ‹Âd8Âp¿¬Â.,—4 «,œÀ5̽,º7¼Ã Ã9À& Än(Ä™Ã3lÄ­úñ Ã>ÌÄsˆÄ«ÄQ,ÅÈÚçëÄX\¨T¼%  °|Á†¥{V¼Å]L­_œ$Ð Ö? PŸ™Æ¿ËÅkìSý#sj€jÇå‹Çy\ºmŒà92çÄD|±\ÈÙIÈ|'r(ýÓ¼§‚¬¾”,ÉX0•ü‚Hýs€|¸ü³Ÿ ʽ{ÈGþpЇ Ù†Êýëʼ;•%p?Ž1k´×ÉÌʩ˽å ÐêÅ©\±‘LÌÃhÌBÛÌe+Í©[Âlͤ;•J¡£âÍŽœË« ÍR°ÇðàLRÈÔ µãLÎP—$pÔf¯é-éyk÷eÒú΄ÏòL~ÝpÊ»11˜rŸ†ruP¹Åj<Е ˜ŒüÜ™Cra„Vs6ЯKÑ”k ×жϨ¶pê%Ñw,Ò#ý € 0Ó©SNÑH0Û6.,=È.= S)Ó0Ô´µuJ¡øŒlBu^=íÉ?›ÝHmÑ–lÑÓÔþÃüÔÇÕŒ0ÕÚFºÆ  ýÌZ]Å\ S—ÖàSÇÖFa  7 ç ùA × Êd]ÖuP¡z¡»0qÒ3ÄÑ4£c­ÍŸK”ðØkÙÍÖM ¢$j¢œÃE3gh}[b-Î~ g6ê;8:NýÌ#ÿ Ú¡=ÉZÜÒ­½XI ]€ÎÐ„ËÆËØš;•#À"ßéáÅ̬ÛÐËØ€FšVî)žº ÁÆM‹Œ]µy€ib€Õ<«%€oùÝ¿ú ·ÅmÍÉ‹=«óq£´Ü\Ð4Ð=Á|ÞÙÝ´ {ß Ë°ë­³SÙ`&pþ “FÜ»ÞPÅ ÞíÛ‚µ±Vw±-…ÆôíÅ ~à Ž¾ŒV%Ppu\áh|áÐá þ `nÞ ®Ç ¾â$.«À³á õá.nÈ"ÞÂ/£û!ðjÐ!güÚ-žã:¼ãû+ TzVß Nä-ŽáJÞ¡Å báLå!~ã$®•̽0ë²kî娇äK\å0þ 0/fñÝRå,ŽæW¬æSéàÜ*ÎåRNçmçŒ-ËoPäEláfÎàØ¬Êõíç° è=Ž®Â¼Ý‹~èàèÎ,‘ùÁè>­æám§rÞ™NéÑméÕ|ä¢nܤÏ“þÎçJžê½ê#î诽é]Îê;îê!]ë±^å¸Þ×}:å»Þê¼=µŸŽã§Û½ŽéÀ®ã²Î¿¶nìÏþåÃ>è‘nä°Îì¼>íhYí…®ëØ.ì>™Å¾¡ížì¦ní[;îg~ì­îמäÍîéðN¦ËNïÙî@èÙœîÁ~ëì.—ùžæõN}Ê_çû>ëNmèê^éÿÜþïóŽðïìí/íü.° åþñç^ñq:ò­¬éŸðíàïŠ.ñ&¿î!_Ì*/ª,ŸÕ.¯}7ï}9/éœ.ï2ÿí_óŒxñ1ïíúî¯ó}~ï~-ôJ¯ñþLÏñDïñWòMôSïðUoïOò ÿçö3Ïð?oíAòió—ðYòFO³HÿöïõE¿õjßõdÿõ õΛöÝ^öö+øZ-õOoøÈÎö[~öb/ø¿ös/š=ï’Ÿô„ÿòu_êwß÷yoõKêcßè›Ïóˆï¦¥Oë§Ï~¯êC?ú/®øsžú?Mû¤ŸùvOùz?øµÏøñîø{ù‡ü¡ûX/ûZ/úTŸû¶ïÒÈOî«ßò­~—_ÎÓïô~Ïù×øÏ/ÒÑÿîÆõÂ?ßß?¾ÙÏõÕŸnïù±ßü³_þ6>þÞOÿ‰/ÿî|þƪûî¿þó®ßý@`‰EP±,™MçÓR€ŒUë›Õn¹]ïÉeóV¯Ùm÷I}Ïéuû@î¡R¹I‰opÉïî1Qq‘±Ññ2Ò*N²ÒÒ IpÐð-p“sêr”´Ôô5UNtÕµ2”¯0IvöUw—·×÷÷”xØ3ﶯµVð¸)—ø:ZzšX˜úÚ+–¹9¹ís›Éy­¤Üü=½»ÝýþÒ:žO<ª›í_\ @@ ø§^B… v™×ðš¶~úÖðç/M¦ 9vôø±ŠI–4qäÉa/¦DcqÆ—¶ðåC¨gNþ«îôÅ2¦Ë30™ÉJsâMŸK™6MÔÓ©+ E…bBÚRé¿«A³Fõúl¨aMM=fÔê2¬sˆž­Jn\°cåZ2{ m™¶xß’Ù++o]ÁƒUÒ%üè.à¾cþ‚ Ìx+Õ®‡)W^hز¢ÄŽ‹i¼é±çÈn'g6}zfÔv6ƒî,v4ßÒikÚ\}74Õ¹kÝ[[L-W¶±Ïæ}¹Ý×É­ú¼vhØÁ%3·~Örì°W‡^S:˜Ï„Âo7Þ¯vô\Z“Wï8gãé©“^Æ÷ù'¿‹ñB™²údãÁá#PA,Úp8‹‹0:¼0¹þÝ0ì¯;û¾KŠÂÿÜDæ4,‘ˆg±0 WQ¼ø\ƒÅW;ñ¸  8°bƒ‚H(BE\XtPF÷hÌ&Im|r;y[@ >x€ ªÐ @ø vRôÄS¦Â%¡\s0)që Ë!`K¡ŠDFMöš|1Ä2Ùü377o{@"‚üÀˆ K»ó‰ò˜”P¾>Ï”ÒÓ]퀈€ Œàrh #uÂQø QÒç*}Õ²KQ+€"F#< ƒ2 €RÇìð@3]õfO#ó„•Yd=Í€Z‡¸5W,D "ÉœtŸdñlÕ»fÅ•ëþYÓÐtN5ЂB‡M\ÑqÜzá*7³0ô­vµ% € –OcÃEvU%í]˜© x˜ެ Î…X€Î,®Íö]z –ax™)|3£RôÕÒ,G¸<ÀXa‡8•›eµpQÙx‹%èœL¶LG}Âe!"0 €p@S·=¶Û„œ×Ï µ.ihqgâþ¬.kn·>[¡®ùûº°«àùÛF›îzÔÎmÛäžpïHëþ;ž»ñË;U=Çî¹oV_¼Áï#Üm©»lª¿üÇ׃\ç½m4rm'½sÌMÏ®ô9÷™ïÊ>þv^4GoõĶýêØueöójwäªEÞxUz7ï÷=~îâ'åø(§~½¢ÏQ }ÌѯOýùî«ç~í郯^ûœY÷Ûûô#‰»ä…_^ù¬ÕŸö¯sŸüáã7›þþYÃ~sâk^þà÷>ùùÃÞغýYN+åÛXA©Qðq Dß©AýY„úÁ` ¶AòO?lÛC˜@ûY, ØB†…¾Ó â˜çÀBð†AÔS‘·ÃÛõЄ4D¡™x…šÈˆ¹ûaSøÁ&^±?D”^ yÈÁñɰ†X£ž˜¡(’Í‹ãÇxÅ2"þ'†UœáØF7vgD»8Å/ÚñŽZlŸ)—F*±Ž€âÇ™¨Po|<¢"ÅÈHÞ8ò(,\€¬g>JŽÑ’"$éÎ×GDRñ“7 %n0I›9Ê1Œ©´á*oÓJ½tr‚¥œ¤,‰Grq—§üã#­ÈËYú²ˆÀ”¢0ÕK6³‚´¼Ñ(·§ËeÒ•ÐŒ&2·XLg&Ò•±Ô¦ ¹9He¢ÑÍ$æ+ÇINAÞšž”ä5¿™ÍvöOš¨±%}¼¹NqÞ“~ù´T·À=°=7Ì0 /¥Á¢{p^#\büv˜•/†Gò(‰Œò)’' !}"2%§ò þ‹’+‘#Ý? `_†  •"ç°‹ H”Ò-ß.ãR.ç’.ëÒ.ï/óR/÷’/ûÒ/ÿ0S0“.Ëp;˜pžÐ¢P¦ÐH`À„'“2+Ó2/33S37“3;Ó3?4CS4G“4KÓ4O5+ó6`Hðc e°„e’Æbpk0ÞÐÖvsq$0 iÓ,G279“S9—“9›ó |“×c«m;J ¨$€H^[¢&9¬;µs€ ä09<À˜f!@€;À;ÿ¤eó><W$ó9D@K.€®%9@[óþ8øÓ?@‘&¾¤læ82à8ànPB=€@À@¡¤bsôÓ:ì³ïƒ”@Äd ®#ET ,±ãCK ?k´E…àZb”MÖÐ,1fñD×cF‡c11Í“9’ty$@7ÍJM@:…" Hô6ÆÔlT` àC­³¥L=eÐTM7d§Åi'zE *q;ÆÔH•”í09Ø40Àv¥Gö7 AM Q‡`QÙÄ «%?J`ÐELUT2U6õIMµ ÚT™þCÌS§QS`Nkä\ÜqKùK(57ÆôNÏ4M­ƒM_Ñ®#VA•SðTi†uM„TÚ0LÇÒY•U«´Ie”U« ŽV·Õ¢±I}µABTæ$AH`œÔZiÔXÔEaT[ݵÚYƒÅsE{”L@_×eT†VÑÃ$€>`P Õ<:€¸Ä8€ØA@;  ”9–KbK`&Ô:@&59è=?LÞpb+ÖC¥hzäHÍ.TT`K¢ ÂCà=ã9nv `H € ÈÕ2À nfgÓiŸj£Vj§–j«Öj¯kþ³Vk·–k»Ök¿lÃVlÇ–l¯c˜N¡L Ó. ÊVnw€Lvjö ÈáV¹õæpWUi•Ô º5p×ÈS  V‡`=+¶G¶ð.t$€K• iàlTg1ÐÀ*×iôµL1à : L·4VCÀRàZ“ BÖ8Äheÿ]×âŒÊ˜à 6è#h POàÜ*È …õIeæ>Ðe”ñ=B˜P_W÷§ØYBd^ lù\Ih Õc 4¦AAL8ă×_šB¸‡i† °€fß}©S’@‚‰T*አ$*@Fà€lçQeF0ž²Q^®Ëµú*¨ )„eTF` P§S§@àÀwFšÀh³6†&@Àc (Z,µþ¶b;ĶŽvÛߌpÁpµÖªÛæCì(§}K”𬑠Á³f" ( èÑ"ñ¦[>Èë—XMšjVªeáO:Ù9 ­T(š )¥ SX2Ch@~BDuçÓYÄ­4'!½ôJ@ïwÄe² Ä%ÝbmYnÐ%«jBž qs|iZ{Ìk† ñ`Ö&`¶ _V,vU{¼À¾ •PBhç³–¡-ÄRðA !,m(,#Á(›&$*àÊ ±µÖe§x©{Lqž2€w¥Ó<2…±ÕB¨Ô¥.Ëö_ ÌÄþÀHX†]fænfIg¨2·¢:!¸Ý:ë& À”q9{–A.y¦Æ¡üßÉ:_=bà®òòt/5Šz7ÁØÚØó®_^Á{¯Dl-ã}€úúUóª¾aϦ¦•£¥ÇoüŸõX56¶ÍdHíŽPŸŸØO¡ñQŠ€„:ù‰Í^~$4ÐI³rS‚í©e0š ó”rL±9X" | ˜¿„0'&œoa–A@G¨$pÀòSÂt'•ð”‡GÉ£PÚ2'„‘L0i鞃Dv¡"˜Ž‹&H õ°s8 iцÛÛþ€&ÝýIƒ]ÔK‚sÂñäŠEàØ^ØrC!< ‹’bOy¨XE ¡‡,xHÉGˆ !l¾#›ÜíÌóq˜ ƒ@X&hH耗„Ø`‘"œNõ0Ö2àkd'Ó* П ðwó‰åŸ4t îɈqþBé tЖÀÐÑÀ“ÃA €àðñI zm¹H¹S·΢ŽMûé¡Z º/Ú¢YŸH£2ªŒ,ªŽ9z£zh£¹£<ê—>j 1¤+ ¤9¤F’JJ Eº¤:Ù¤,(¥Pú“HŠWZ¥öð¤GÀ¥ZŠ•Tƒaú¥ò8¦3™¥d*^Ú¡iªkJoÚ¦vi¦®§r ˜tºƒyz§Â`§È·§| ~ê§Jƒ ¨…:ˆZ„‹š¨ºp¨hꨈרG©’:‹“h©—*Œšj’º©á© *¤Ÿ x”:ª² ª¨ºªºªùЪ®šˆ§þÊ”¥«Ù«¶Ê™µj¥¹*«»ŠŽ¿Ú«Ê7«¤@¨Âº¢™z¬ºš¬Êª¦ÄJ…ÏڬЬN)­Ìúƪ¬¸j­•H­Sé­Üz–Ñš…ã®` ®bi®í°­êŠ©eW®íÚ†ðê Ùz¬ì¯»8¯çН©ˆ®âê¯üê€úÊ õ*¬÷°å8°h¨°;˜»—۰ʰò±‹ {±Xz­›°K Û«Û±ù±ŠI²Ñ8‰#'n}†²Šj²}êp0ë²wH±˜€2P:»³<Û³>[`³4{¤“Hp´H›´J»´PB;´mñ´5k´L[µKë´3 µ(µþ”€2Tkµ`Û´\«µcp¨_¶U‹µd+¨c+ ^‹¶`«¶kë f ·V+·sË uk·L‹·y;±Y+°gË·Hë·‹±m»µƒK¸b¸‡KЉ o˸Ik¸K’Ž«¨‹K¸–{¹Sš¹z ›Ë·ë¹b º9ºv[º¦{¦EK¹•¹­k¨²ë“ »;»˜ûº¸›»ºû¹¼‹»¬û»ª°·Â[»ÄëÆ »Ã›¼•¼Ì‹¼Î £Ò ‰¢Û»¾;½zŠº«ºpÛ¼Ú;­ÐK¹à¾äʽ5ê½h[¾æë…Õ+™×Û»ìÛ¾ûú®ê¶óK¿ ‹¾ˆ{¿qû¾úû§üû‘þ{þ·¬¿ËK¾L¿ ̸ùÀ7»À®¿Ç;ÀÌ ̹l¾Lº¾YW±{Á*Jz½|ÁDû $ð,’7€3fp»*Ì´ o7ç÷†Âœ¶¬½ÿ(]1|°XD™Â ¼Â8¥N,ð!-„7 Ä@Œ½üÄn;ÄQ /u„7ðÃeËÄìÅÎûÐB!`l`Ä}‹ÆÉû%`8’!u÷˜f¬ÁQÌÅeÈ‚°°  a`ÃM,Èç;¾gLÈŽü¼ºvL¼ÿø¡"›µt|µ—ü»ÿø@Ê FeüÉJ»Å“þ¼ˆ¡,$à%Lð™¡YœB€u5&|¬»’üÊ\ ’@ƺ;Œ‚Ë3g@$ÌÄÈ‘ Ìõ[ IüÍ™Ç6PŸ½\¸±<»Y @ÌUQžuo–§ìÁ¿ ÍxºÎ|°Ï p’¡ÈEPÍÊ„lÙâÌ€ÌÎû ñ ÏÀ`tKp E;4¼E5ÅÏêìÏjÙÍb€ÐÂôkÊ…ÎÛ|´® ÑìΓ@ÑÇF§†ÑZ,Ѧ‹>—Òà)ÍÒÔl0€bÎI†26Ÿ¼ÑýÚŸº GÍäãn¿;>§1nà<žâØ*âøKâA¾ßCŽÎ¾•Fþ¿»Š2IžãK~ÛENÛ`:åÏ\åV®~X®ã2Îå•<áQ^â?>ãc>âe®åýÌå=~ågާlþÐn.ÀÜæ>çÍ]çi~äkç ÞçPžå€à‚nÀæU~èBœèB.æ5îOŽè„®èKÎèuìèJéMî¾_žç…>ߘÊšNåœ~çtê–þ>ä£ÞÊHêéÝê¾\é¾è‘.kMæ´¾é¶Þéô:é¾ë¦Þ먾çª^ë—~ëqëj.ì[îæ²ÎÍ¥þì§îã«ä°ÞѮѯ~íh®ì ùé!®çºÎçà>£â¾ÜäÞìæîë`™îliæÞŽâÛ®ÔêžíØ}înÀì~îìx^íp>ï°'ïÈÎêúΑÀžéþžêïåøn{ÏëÉîî›ð¤¾ðÆÞðëjñ®>íÿNìÖ^ðØ.ð†~ðÌñ³æ"ÿí¿°ðÌ?ì_ìå~ìoð-Ñ/?ò+Oï&¿ü>è*ó<^ïs~ëÞïíNóìnó2óL¯ôNOíþ ð=?ðI/ôÐþó+¹óZ åŸÚFßíW_ò9/¯^/çaÚcïñ _õOòVörë\“(/í_ówyÏínŸñp¿ñiõk_Ùm¿÷Mß÷•Xø~œõ”¾ô!Oô_óT?ó“ÿôöìøsoùùVÏù†_÷Ú~÷núøøC¯ù,õZ?î®O×°?õ£õ™ù0ùÁ.ù¥/ûjúùîùo ú”)úoOúq_ö¡ü /üÏÏúÑûnû¶oüRÐßãvã«ý÷ÎýQ€2 ‡üU)þñ.ýàpáð¿³þ·êÿûØO½ìßñô¯ú.þýËŸÿ@`Z* ™T.™M&PN©ÕiñèÔn¹]ïÉeóV¯Ùm÷û ÍÂéuû} ¥XùV¬JªoPè¯.бÑñ2Rr’²LÎ2Só`/±oñ“/4.Š´Ôh“µÕõ6VösÖÖV/Õo•SP÷Š—nôwÈôö9Yy™ù¬¶š¶“xN•º0øÚ—Ú8<\|œœÓºÝy:›hû´›ø›mØÛ=ý?_úyß_+;ykèųצà¯ÿ6tøÐM?ˆÿf[˜&¡®‹h2¦Ú8dH‘%޼W±Þ¹ˆØ,œÇ2¥I™3iî+YSJþƒ*ÂÜ È§B—8‰5úêæQf:…ò| ¯)P¨‡*µz«¹¬üÖµtJ0(Õ¯j:’ú¸mZµ«®uÅT¬TvíÆb ë±­[½{³&åË .ÞºïšÍ«nªà¿‹cõÛ¸R`ÃÏ=[¦ì§Ë9wÖ÷Øó—0ÀÁÉ«Y“X"Yóa3™mC[‘ìлyƒÝ[Ë><°¡‰@|¢Äë®1{YÖM÷ ÛÀ¹w‡õÛ{’Ç“0¼*ìÚ×oŽMy¶ûõðÃ×·™=÷”¨þÀDµ±ÔË>ìä#P.ò»Áµr°‰P"2`B¹þ ž˜î§ê,°½Ä&ƒÅ¯¾Pb0` :ø ƒðP‰µ[ð’$QŒì@RÅ$•ÌÉÝ x1‰gÜB„ίÔRD.—üL‰ “¨Pƒ.à¯G£ê’:a„»¦¯ÁTšê& Þøi¡Ñž{&µ¿d›fˆ·.;kºý&©í»­&ëoWJyÈ¿Éî%ñZâ¸#_œr†WòqÉUæÛðÊ=Oçr<‡zl³ßûóÔÑ ]ÅÌ9gþùt²UŸ=§À]îÂaï ñ_iÿÝ7Û1Ç}rÝÅŽÝtà•?†õ]7žðÒû^žzYš—øÍ¡'ùé«ÿ¾•ë!|^úΟBüô^ô°£ïÞ|°z¿Yýú5ßAòáßý|Ùíÿ_økWö§¹¾îxT #È ýñ}ûKà)(§¼%€¾Càû è¿ ~ð[;˜¼þ™„)$Œ×–Aúm/wå㟠i(¦¢=ð„Þ“ kØC€±¯u.tÚ¹WBú‰Lb¡àÜWÄÆ,MØ“¨ÀêKˆS„œöd8Á*~Q:T^c(A>q†`TãíƒÃzþЈPTãÙX7Fq~C„añæØGÔ1E:@y.´”ÄÄc8WZ?Œ6F£LãhIçåO”Ö”¥Í¤æK?JÏ€ÆT›@ßM“S“ú´£·,ªRmÚR’tš'*;JÕ¥Z‹DUiÓ8Hšz•zL]ŒS{ºÓ¬B•§hUœZ_†U¡j5®z#¥\Ó Ö6ÚU4*Lùº<ºò…­5së]áê×Â&ì°<,E…9Ø©>Vy‘UØdS*X²š³³Ó¬[«×qšu¯¡­cÉY©Ž©õT­êF»–Ң쬙Ìíl+WÛ£¹–«„ijyë9ߦå¶NÀë[[\òv‘bõll»:ÜÓ:wqÇEKrÝÖXÆ6»þ¿ÓîV¸û!ðv›áýÜxû\ËÂÖžrT¯ßØ;5é¦÷³2Eíuç;·ú^¥¼Ò9ïk§Ûß¿ý×kîÍoRuK\4”à9–ð„)Láè8”н¤‚©+ÜýÞ“0¬ñˆI\b¡b˜&VJ€Û4àà^,¨ÀŒi\c߸Ð0Š¢â£°ø‘.~o­ b#vǪäp|ËÚ`þ²r·GÞì‰ zß„¿†q‘å+å)ßpÉ\nò(Ÿ L!{¹©I¦¥•+Šå+¿¾b-šI«f_†ùÃ]†²ƒÉld:#×ÎÈij“ýlæ(šÏÞn ½9è>ëÑeVæ¡=5*”Í•þ]°ló<æ=KºÒ ¾´K) ÎDGºÐ“>u¨E æLÿÓÍm†3‘;=gV[ÚÕ¥î癵çZë÷Ö¸ÞéÝÀf™É¶öŠ}ÎWÿtÖÈ–3°íéeÅÇØvô§SmjPïZ××Nq³÷¹mTCZÕߎh¸Å-“l…Ø46eaí7·»½£¾ê³·:diÿšÁÕV¶Z \aƒ:5}7Qâ½Üï²[§Op‰)^qrÓná8iø¼Ñ{ïMWW²2ÆñÈo¬c}0ãi3wº» îU{»åíå59S>n~ç·/w¹º7*qfÏ|›5¯ÛÊaŽî¢[›åF'/Ç-*tw}ç1ïþ¹Î×Mõ©=šN7ÉÆ±Nh¥Wç¢AøØ'|ád8\áÏ(ÔÁ.õˆ[ýíaªÅé.â“O;à±ÔúH¸îsÅúÝ´nªÈI^ø«=ð_O$âÓ|sæ>çro+ä•ËtæY^ {_Æhtt-`?@€ºëø‡Ãý©]8µ¹­xJ }¥šW†«ŠC&”ÀØ€Òc‰òÝU}ë‘~ô'½ø™€=HeŒñÈÊšjBx{d+øæÍ¾€Qýd³þÜɃ÷ñÎé†ÿvw¿Êž”Àüg 0€0ð,à£~òøÿ»ì ÔkZ{]ü ÿ¾/ïüac¡àÈŽþìÌšïàúÎd9˜€C”`<~ï‘LïØ<ÎÿôèÎüV¯Ëàðøô¡ߢîVñ*Oý¬ÂE`DF˜ @ >˜¥Ø4Þ Íß®,‰ï£Îõ¾ Ãoñá7A ÏðLnðžF`J’ jðsðþ$oÿ¼0ñÐe ü/ ðeíPŽÇPù0Zp"$„B&p ÀSà—€Q‘ ‘&à8$Ì6€ÑÑ$L5&À/Ñq1;Ñ'1Â*GQ5QÂÀSñAñ9D‘GÑ#lUþ‘Y\ñ/1Ÿci1mÀUÑq1 qA ‡1q™q)‘ën# ¡±'챑‹,ñ31e‘!ÑË1Cñý–ÀìÜþ”€æ/ êZ’@5¦Q÷‘ûÑÿ R ’ Ò !R!’!Ò! ÑB.% ¤ ¨þ¬ H`œ£ ?$CR$G’$KÒ$O%SR%W’%[Ò%_&cR&g’&kRÂ>`\cI ^%V`O ÷t÷ ñ )ÿ†ó`Y’ÀYîôJcô’’*«Ò*¯+³R+Å`)þ=OIppÄ^°&J „$à5xÒJ°Å>ÈÒ,ÑÒ)GŒëÃ`Z€¬xR€-†ö~2I<@F<òð>D@À8.@ ªdô8 ÷t2<“1À1“`Ošã9x%<2à8à| B  $s(“](2S1„0u0IH3• àÂF " Ap33‘Àø ïÃ5K`1‰“7‘ J€3_Ø/ ÜO,‹mSE„ µ°"çÒ>´Ó)SIäϲлaìPÓDE³4Ró(!›¥Z` À5$ ¯¤>/$þ ðs6‹… £d['nD 0P>s ® íQ?- 2< FL#A?3ŽCëÑþðÅ@¯pIH`ÌEæóD‘@C‘ó,µÀý ”A$À>M FM þ\ÊäÙL F´>æ³@ï3?qt8›  A|ô +„G£%JÙE:‘ídO˜4<æÓ<•€<7tJ—`@ä8 ¸3S €L6MÀ<ìFe´B§t7{ó7ƒN—`GïãMáR7›“>QñÅU`… “Ä$€> B'EVS9€8 : ³:`2ïCSÀ:µF 4= ð“Níþ£þÀ0áTEµ5&YL;!JsC :k¢D,R4Ô²þ²EHlH   X @ê44 ÄzY•u+·•[»Õ[¿\ÃU\Ç•\ËÕ\Ï]ÓU]ו]ÛÕ]ß^ãõ!$³Xf¡>%’ . äÕ_‘7eF X»Âˆ”Jà_v‚”+Í€ZÙ4 ª”a+Öœ…5ÀG“/EÕ4¼ï Z$@9îqD65ç 8UôL àJsµ>1à µê“=}4 Z@'GŒE-–h)A”@ Þ4 ` `÷SM5O£Ì#EþëïBUdy¤8 àCIO8 <@Iϳ>`pEbDV|Ô. W@aÿ(òRi‹Vo%¡6‘`c‘@G„RÎÓg)SBk%Fγ<­…8m (¥_2°¥>½3_ý¶AM “€n÷Ös#¡oM€i‘ "ðq¡¤“ÀGkE9² :v³qPd·9ê:ÓöÞô/©O';÷s—NôoM “"ê¶Cí¯þ®•?ãÏrí³f}”2•ƒ-}x·—3rG×ðR `hÅ7 tר¯(Lr—`v“€0‘CJ¨WVÞ”2UZ´—{÷— BwxÍ÷q‡¶êp“àpM`~› 8Ý"wVìoz“@g1÷~S4uùׂÓàS†·x7«–®VC èöF6 2óK3EÈÖlm­Pß6®Ø`}=ó‚{ B|;Øe‡HEV" þÞQ-õð÷VØeaVfi6sé3wÀ†¡%DˆØ‡»¸Òt ÀØ‹Çx `÷8@jõµ ^–ŒÛX"@G˜R»@ŒÝØŽïóX÷˜ûØÿY™ Ù‘Y‘™‘Ù‘’#Y’'™’+Ù’/“3Y“7Ùs‚!ùd, X… 1"""+++444<<<Mci|CCCLLLTTT]]]bbblllttt~~~ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@”pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´H€ Dgþ„!>{î”´R‰¡i à盢89v€CààÑ©6:êjQ²D›²¡@&”hBõÈ$’@=3 S§fö¢õb‰ª2™`€ P0RB«b@'H̬VHfB?ɼ9 ã&L8 €&uÜ-m„0™¾Ñ %b»0Å[U²¡é "Ç‹nh7åÎ}((0‚ˆ‡Ä!–Ä.Rí"Îùú ­:yß5!}8’Æ+Ñ D†Ë[B<Ð: h"°•'à&LFÄKgR>U€À[S$ì·Tþ*8DòéA†Z5P  À"¡SÕQœ|Hl· wz55ˆUXD f€É 5"Zà€:V`ˆæ¡ ‚†Ž•&"‰+¾ [Ž™ ”ÒQ%fœE€åK¢÷ÐL€Â}îñœðdW›n¶§Dÿ(Ö Á4VšI0A08‚peYÐ @‰iÉYLPé‚òÅ)ˆ ÀJAkß !OvÕ§DkGt—+S¯~êŒ964PäQ tj+b-¢À¨²ˆBxÅph¢l¦S´°‚ @µþ×:Z†À¨d@AuÐJknµj.t§ÒQÀ¬˜€±oÇù™ŠBΡ“"ø€!L÷ß L7©ªEh[T»Eç"¼õŸN ^¥!7È A_ 1UÎG4·ÖœUòÊK¼‰ +ÔÉ(œ²Ç…°U ÐÌÈBÌéÆ{Öñ¥@P@Å\<óª( ¸5 #ìµÇB„wöÇ äÛë‰ßŒ`€'Œà3º ‚ÿ*ñÿyñŒ¥)D¢ÇE™C´9ÖCh»Ûƒ˜BMÄ­˜ë4@‘en޶ 2EãQ @þÝG0‹/Œñ\£˜¼S@^H%ú¤£`yœ#ÀÍ( €–cÎ5ØB0*9 ”o«¼ÎC9š÷äm:1ß½Ý ¥ì¦ÞÒó«Áwe'õú((¬„œ€?êE°6AÑ @Çw€ÐæʃZç Luf\EàÓŽ²¶ÜˆsÜkUÙ€„ŒyC°ãR`36êŽ\Ô¬ g¬ž “£³ ¡cB`Í•ÿÅ+€ gl(¢@‡EØŠ™BD } 9Aú¥«Ÿ%1$‚åÄb*"noIðÀò§„îôNLÙéÿ⣺ZtF*œþ0Å«!8'c«k”ê(„.„BðYœFg5ôNO)dÝ p>¦M-j,BÇ€'Ÿ)’’: ÌxF_U—,5IJ'‚ŠA ïÎH?VPŒ!Ò*[ûVÊÉà_ÿ Ë!ăòɘ‡„ ¶7:S´ÓÓa&‘zCðcçY¿ öêvEHdó~—£d A‚0äæh%K/ÈQ¢&YÓ˸EɈ¿$£$˜'¢Ò 'ˆ€"ÀÏÈl*¢Oc”䳟üdMª¸†u —I°^¢>å%O4å3&`4;B‡™ÐI3êNÑÒ››¤æ‹bþ ¿iç”ÜAgðd…:ŠnñB4©)º9hJ”…&5'O§gG¤,±ˆ0½§B¤Ó>°TU æ4¼"Ø ûÏ^I>±É°iz’ÚØ<Ãùœà-›9AÕd6²c.~)ë¨öL ø`cUƒfQª-#H‡:ÖÁÎK{¥´™­Ïi4e[ך–(¡¥ŽFlÛÆÊºD6à«'Гˆ$yʶ%‡cÊC J?©T†\u6D(Á]+× áªFÁh³ÕvJ5@V 0„tI`]}½c~Ë®œj²­äš€¹DRÑwŸÀek”6åÆ+¶&5«iM@w¬EÖfS`-¨\ þËF-«YE"mõÌ›-éŠf*¸Õ-\JiNã™×‘¢QÇ p´êÔ3©¥Å"À¥ &@…°9Ä)ò;&€Š`à$8ø³5‚k„@`¾4 'A\ª Ç2íH+>:êcmÆ% ˜¸”Òd5QÖ!É….æžwɇ¡ !àzø‘\ÆY&•ØsQò0ˆELߤj€YNZo²B2©7ÀYx61€Á'0À:€_¡¤¯{hg7jæ78¬@(BqŒü¦Î¹sÕ¶[ç7§g¼µÙGò%L`üM¸Zh7˜ MäçÄàþ¬˜#XQÑŠ8\é7€ŠÏÃYE ]êV¿D:RM¢h2ÆjWÛ:%÷!õ‚6}®"˜ !°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØÎ¶¶·Íín{ûÛà·¸­‚8L÷É£r-ú|,Œ·¼çMïzÛûÞøÎ·¾÷Íï~ûû߸ÀNð‚üàO¸ÂÎðy·/÷&¡pà4®ëÀ¸7Îñ޹ÈÅ ò‘›üäØ.9ÊWÎòfOEOÒ ‚‚`{/­jîÀxs”Q<”¦FЧ1ôhˆàçÓðÙo>„ØÁGGu5Š ªþCÃêΈz<´Ž ¬;ÃëÍû2¸þ²[CìË@»2ÔŽ ³·ÃíÔxx5ä÷lÀ}w¿µÞ‘÷tô}ï€ïÃßÏ1øÀþ…/GâÏø8,~o¼ä×ùpT~ò˜/Ã忱ùÌ{þ ïFè?Oú,Œ~§/½ê§z»#}õ°'Cë±1ûØÛ µ·FîoÏ{!ìž¿ï½íƒ/ â õƇFòOúågýõÌ~œß êK_òÖ;ô¯Ï}%d_ßï>àÃßöí‹ÿü>—:;È~W³ßïo¥ãO úËÌöFþï¯ÔýÃÿüEè €ùR€¼€€è ¨þ ¸€8ñ€¸ Xh X2‘´Àø( !ø,1‚°`‚$˜(è +˜‚&Ñ‚¬ƒ.82¨ 58ƒ qƒ¨ ƒ8Ø)…Bù =9”¥p”²W”HÙJ9OÙ”OÈ”ÕG•R™QYy•Yh•Ú‡“þ\É[ z^–1–^€–f‰†e™ j¹–mØ–ç—(ñ–[`—ty‡ry x™—|¸—ð˜~©‡‚Y…9˜x˜ú§˜ˆIˆŒI '0ÑÒ2Hobrn}Ù˜‰ –$Ð)–£nC A Á8E™šyx‰@ ùM£ˆš©Yxiô#¯ù˜±éˆ`y ´)qD AwAaOå{¸™›¸›Æ@›­™6ð!°*2œ°iœ®Xœ¤ œN@˜všÖIÚˆ ØéYÁÈéaØ¢pi¾™ãIG‡Ÿ@wæ™%ŸBt áÉÚÙDÓYŸÆˆžŸð Aàeþ_›Ñà  ÑIžŠoÉIb2·²Ë‚öUÄYž:ÿÉz¢÷X¢ˆ¢&º[§¢+ª‘‰è¢/:’2Ú5:£2 ¢Ü0¢8Ê7*‚?ڣʤ'H¤BZ•:Ê¢Gê”Fº’Iº¤‰ù¤0 ¥éÓ¤­À£Tê‘RJ{Vš¥Å€¥P¦^Ê“]º b:¦šW¦6¨¦hú gÚoÚ¦Zɦ?¹¥rzœ-j§w*Œ1ª§{ú}ú§kH§§§‚z—„ „‰z¨6ê§9ʨp¸¨E(©Z¤Ž: †Z©V©IÀ©šJ¢—z“Ÿºžz¥:ªOpªŠªø ªîI©¬š ®þš~±z³ú¡µ¥yš«õp«R«¼:©¡”좨ƺ«Èš¬e¬SY¬ÌŠÊ­º9­ÔЧËz­ê૾š¬Üê¬Úê…àJ…ã®—ð­Ðj®|Y®]™®êú¥ìº Ýj¬èú®¤¯âê®öº˜úú•ûjõú¯½ˆ¯™0¯Á°KŽ;† ›°›Ù°lÙ¯» ;±'*±n ±û‰—»±ù±ë*² ; [²SêË«'‹²4J²ð ³.ë¤Ö:³-)³úر6»-»³ÇÚ¬8ë³uZ³BK¬D[´F©³r¨´HK=Û´Ìð´P+ŽA +›«R;µs™­þZµL«—UÛµä¶nz9Ø&gbƒ_ûG'pd›¶Õù¶½ptpv{·x›·z{°¶p¦~ÛGgP¸†{¸ˆ›¸pû·pÚ¸‹0¸Š;¹ŠË¸rë¸z¹ „K¹ž[¸–‹¹–Ú¢û¹”º¢K³1Zº¦[¹›º¦úº˺­‹¸¨ »f*»‹È¹µ;¹·‹»²ª»ªÉ»½›¸¿ ¼…*¼•H¼Å{¸Ç‹¼I©¼²É¼Í ºÒ »ßJ»Íû¼Ð; Ù[½Î{½©û½àk½šÛ½Ž'¾‚ ¹å»¸ê‹¹ä[¾Ü‹¾Ï '°Ðyá •Ô[½óK¿c &Àþ,ðs ;iª½ÅÀÜ®À"‡ósÀn ܾî{¾¼ïû0?€ì+¿ü·ûxfMós0ÂdÊÀ½ëÀ\°'ܰ*?—¼”0\»2<ÃçZÃL €@#  kbPÂàûÃ@\ ûx>sÀÌvWÀÄ,Äb[ ðô¹Äÿ»½ZܵñÛÄe¬µgœÅüÄdÚÆ†Ð}"J±cdÙíëÄn¼´p\ YQŽä¿xlºz¼Ç`›œPkÈ!™¼œ(àt fXLÆ}¬ÈsšÉŠÐ;,1³Ã(Ë™fklÕ6»?w¿0œ†œÏ`Ö©jÕðHGy&Æ?m¼l=³)œ½–6.ljǃí²û8¯QBÃÏóÂv×| 0 ÖÕm»²û(¯¡VTÑ…uÇ—Ù‹ü OeçvÁ]Ú%»´µmÙ·}×°½û¸ƒiV׿Ül;Ø$ðhAý د­ÜXÚvÂ#A&ÝÉMÝÕ ÜÐÜ,ÚÓÍÝà, ÷[ÜpÜ®½Ýä]Þ¿ær€¶`°ØˆŒÛ «Û0tíÛ&ìÝíMþ«¿PÄo`߸Œß+ÕN-N“môà þÕbì¶.Ü®žÌB9¸{Ûá{Û·žàŽ’^½ÖÅzt0ÞÎÔ!þ“£­Ñbœâì½â,Nº*žÖ ãÿMã¸7ââ=ãi™ã7Îã¿ÚâÉûâ†;Ðy­ãhläá&ãCÎãPÎØ'.ä@NäEnãYÎ(>å4^å÷}åLÎÆZ¾ªÍŠäæ+å]Nå>îßMÎæ;~æžæ`nzX>çt¾å«{çXðåmæo®Ø%nårç{Îçvèx^æSèb~àdîç>è¶­çdéè é–ÎÔ>Ôš.áœîäÑ«æ|èf>ê\þŽéAêQ®ê}Îèžçˆ¾ç‘®á“.ë•Nê“jêJî€ÎêZ~맻ᴞê¶ÞéÈ-ì^~ìžì¼î½…>æ¨þìtNì+ëÌîæÑ®„Ó.éÕ¾éо굞éŸNÞØÔá.êãëÛ®Á^îgžî‚­íò>ìÊ®ÝïÞèçÎÝôNÚöŽìמïsêëÆîê†닾ﳎðÔ®ð*ûí¸¾î¯Þî ïÍîðàñŽhðoíóNðõ-ñÅþñâ>ðÝ ^òŸðñßòÿòßï$ªñÏñëGòÙ.óOó;ósŠó,¯ó(ô#Oô=oôÛÊóê>ô6¯ÜÿãþP¯ëí=õI~ðQÜX¿æUÏð»Nîßê[Û]ê_ñÜ.ö ãJÿô@ôe¿äs×gÿëûöõžèŠóuŸ÷OÕw¯õVî"¿Þ`êÕƒoòìŽòlòd_øþ~øAŽôæNùÔÝø>Ÿóqßô˜?ù‰oø)?•¡ïö‹/ÔœŸöcï¥ÿ +¿ôIŸú½ú³¯ùRoù8~úOûï¡m>±¶Ÿù£Õ÷úp9ü¢¯öðιþüw âû¬ûÍÎûˆª÷ßùEï³Êú¸/ÖØOõÜOýˆjýÍþYïø¿³Ýßû߸èïõãüûpOüÌÏïïÿ®íþý¾»@pÁ ‰EãsˆPMç•N©Uë›Õn¹]ïÉeóvŠ–j÷¿²-H;RÉ|³…wÿ0Žïï/Pî1Qq‘±Ññr‹M/²ÒRŽÐÏÐmPÓŽ ïó(ô5Uu•µÕUnòUv6³Ô´moÔö–R”twv˜¸Øø™,6™y±8¸7Í:IØ·:ºy›»Ûû;q||ìúúüzZ7<^~ž>Y¼ŸÊÞLŸ]:wÕúå3xaÂvÒâÛ·« ²ëžù1`C9vlrÏc¼‡¶"–›°¢¿“S†tùæ61»,URÌEþ’UþBIhP¡¯fEfóÎ0:oò,Ã4©S£S©VØÒªš0àaJcÉš€‚T“R0PÓJ5é“eV¹sév)Z÷LBDÐA D„!âÄYu޹͹r'Ö·Ù¬IÆ{³Ñ»™Ã|øëÄÁ€ÀRÑRû…­iÇK!7µÌvlŽ›es‰ à‰ØQÄb-]hõÚÖQ_ ‡¹vr峃/Ÿ‚àÀ“6D<€€#¢üÞÔÜKjàÅQoKÞyzõGÁ¯oRÀÁ“4DAáCˆ ]¹?ñ~ç´ðÌS =»Ͻl†¶Ÿ€O>ú¬ !€cŒ þö¸ï»7<°CEÑ•G„N:ª³ÂÜü[Œ² <Î5A@PCuÜq!¥ˆ €ÜkQ182 ‰³‘Æ%siò<¥œR¸ÒrTÏ3°šh@´ +¼0Æ-³àG%¹3©|ÎîÜtN/¾ü ¿JøR@Ѐ?2SS 4Û$ôL6ñ˜3ÎF©4qÄ& €¼DöDa`HSÉŸž,Q, ]4UG]õÒW­øïP&QµuPYuÝu F_¥•U\3luVEyáÙ_}uØcOÍõÙa“6ÎX©ýFh;1Ö?„’ÀkÅ…uþYG›í¶\)Vuv[pÞ­½öÜ"¼]ÓÝ£¥(Þ~œ—Úz‰¸·PníMWÎ|Óô—áõžV`mö5µÝ[¾X¹‡“¸2aù8.ŒEŽMcd9&8Q…k¹‘]Ƭd^OFX1•ƒe¹Æ—u¦+æ]g&¶ŠuÑZƒ¦yç¤ééYן=¦m•žZ(¦eug'+–šê®a²ZÙRCÞZÚ¬£ôí—Àf6Û²Éþøm¨Óž[£µÍmî¨ÝÖ;oºýFÈîF±Ž{l¾åþñz¯ïÃ-2ZbÂ[Nœò¥‘~´ñÂÛ±™]Ã5¯ôo‡spÏ'7=çÐU÷fô7KßþÜbÉS_vfZÇ\ìÓaçZ ·®x{.ŸòõÞcG]ëà•§ex)‹Ü÷ƒ‰žBhé—¿^–Û‰Ï\wãy‡þxìÅGE{繟}÷½Óï{üö!)ŸÜÜÑ÷^}úÙwgš?|ûï©ùà!àÇ£çð{¬ß˜‹é î|É[ßÿž¹Ž5ƒ6z ãä'Aÿ}| Ìà³AÒEðl²ó`HB–Ç„®Ca¸ÌFC¦ð…9„¡ÎXÁè-†½úaätXÄ5q{ÄáA¨ÀûŠÔÛŸgø®ÚyKŒâå„Dó)‹Lì ŸÈE.pG=”È/xE+šþŽØò"ÿD¸Æ:‘‚q4"u¤ÆÉÔñ-Ô㧘Æ*êˈÌbE>’Èa#ÊTeAJ6Ò}”×!VÃ7Šq~˜,¢&EIÖL²yÕ¢(ÏXÈ>rre‰ì¤,cÉJBΑŠ`üäLjÇ&Ú’„¤t)sG˜æ¿`y3Z:s‘»Lf0] Éfv”+ôe/§‰Áe*ˆ˜åA¥UIÄnR—†Ô¥"³©E2æñœüf‚Â) q>í—ñ”g57¹ÎYF“¼ ¥>8O÷ÔsFÈ d Š?ƒ:ìš¿ód@ߙφ¶ï¡êAè·*ÉŽ^{MÏFñõÑbþ¤!åg)#j½‰þ³ŒLi&W:Ì–®ò¥µŒ©4g:>‘:‡¤»gN¡ÙSš¦ó•þÔ©@µyL”x?]NPSfRqZªª“jÆnjN¦ºs›ͪò¶šªVr¨ÏÄæX¯WÖÚœu–L¥\‘ÊÖ‹¹U6p-VZ S»’µ¦ÌTjQ¿*Ó°6õ¯«Ã+ÉºÚÆ¾.µ¢ÜL¬V ÎÆ^r¯XMèS'›¸ÅÂF¯A£+>%ÛÙÊ}–3¡-_wJQÓ†µ™Q­Y[Xž¾ö´•¥çeçZÛÈŠ·žÕíAyKÎÑu­ÁÍm]û©YŽröªÐU®×b ³â’¸ …çtçVÝËÌV]þÇU«DÝp‚ܽéU¯zÃ]Ë2—¥ƒMîaÁêTAJ„,ùÕï~ù _÷2Ç¿‚unI¥»ÙûšÄPð‚Ü`_ÀÃý/à$¬Ñë"—¼%ìo;aÆزòÍpkýÊáúÚ±ÀæY…GzáñºôÅ8±WUüVÕÅž¯}ª]‹Öغ Þ­ˆa¬ãÛÖµ<Þ.ñâÝ&ç˜Ä¥¯a•üc&×ÅÉX†2’K<åÛ¢øÀWÖ²‰Kd¹ÈQÞ°—“,æ'“¢f¦1›»\åÒz4ÅnÖ̧ºeSÌ=Æs˜õœ•,¯XÎŽUóŽ½ä“ºÐU9ô\À›° õÒUþÍs¤2i¹Tºf™F«¨ãê[NSšÏ\M4fEkj;gwЂ>õT»Ø°>ö¦Õ `b;Þ»ž7¤£Ëïz»ÄÚTi¶ÓmizúàÿVHÀio^G»ßÓŽ8·®ìpçÕáû–x·žÙŽWÜ ß³»±«ïmCœãþùFD>”c Þ×öÄQžó®ç9G H[þk’c8Í\Îu¶å½bþ釭ÒA›ñ“Ï<å7þ°Ô™½˜_ýÁf7`·“­tå+T0Ám€„÷ç3V´Ð×ó¢3,}¢›œæP÷‚ùóM샰_†Ô „œ` A!xBp)l…Ÿü‚®ø þ4Žâ/ø´ þŸ®èðaQAñoñ&¯è¯ æ£>žàòoÿúÏ÷øDPøò棎5Íðí ïý˜õbPþâj;E°Oû `,Ä ¼ øo,@‡‹Ð- þ½@ šÐ Ÿ £Ð A½Ä¢Ž 0 ;` ¥Ð ¿ ÏË ³ ‡p Ñ ¾P ¡0 c Ë Ïð¼˜p ë° Eà á åð0è°ÕðEàþp ï0õÐùPü¥0±Ù0½ 1§0½Ò°£ð3Q«€BQG‘K‘@ ç%± WÑÅÐöàD‚þÄ j! v¯÷„${уQ‡‘‹Ñ“Q—‘›ÑŸ£Q§ñ_ÏG*ïK 2O6/ L  CÇ‘ËÑÏÓQבÛÑßãQç‘ëÑïó1C ÌNêî¤ ô¤ ònïú.noåRg$…R,Å ² ÆŽRÌŽ!/#3R#7’#kLRÀÎÿ¤„ÿô«BRô(à,àS`TDä$ %Ÿ Sò+E  @ÅzAX²B^²_êN ©èCC€G„ ü"ž€BÌÎôΤ)Ÿ¢2"þ 0£?ä€/<àþ® ¦ƒ*­²_°.Ï(A0Nì+›à ½J &@Gè’&iqG®NÀ)›ð² („/ãeöj1§„ÿBrJü² =ÐòtD(Ó0“Jro0-óK0^ :°:â’R ÒG4“r° & –RA4S@@“D:ÐB^sEbs6ãQ@eòƒ ¾G432àxoSDÄ67@@à>¼‚6ÅÒ+Q :€:ãŤï8å 4[s+ <›`ñt?7²Ï>”19³öXS AD3;Pÿ,ï&å³@¥À7I$F´ @ó-Ct\Ü24\ÅÔs2Qô.Û‹BT23Q”>TANGŸ F3~^ô‚R.§(Àôc'“D>Àã<Àc*A *€7DJ©ÔJO `@àdÓ6t9xÏ’òö¦ò>€-ûe+(%ìÞDÎ27 $¢ò+vñ'[R(äOµLàº" €Ôt9€¿Ö(]²#/S3US7•S;ÕS?TCUTG•TKÕTOUSUUW•U1²*þÕà^Ó¥ > Usuì¯ME út0E K¥ "@Wõô“ 5ñ–u  A‘UZu,?`@NuõB@…6 ª³Â5zÒ ªuX’óÀBà,àG_S2`'?à59  ( Fà@%üQ¿`ZH ó4@FàD Àø.So:¾âBcy/¤4\_²Z ;Ï`ÊTàCçcÀOH`>ð? þ¤X?"]Áaw6ŽÒK²µ ºbSóCö*™sOôòC3T óî¾pu `T^“Eiµ_þïfÑ n–g¿ö |¶ ö=®¯ €ø¸Ï ðsO¸11ã.–Ãõã5Ó@ñäD…2óüÑkÁpÏ àgeõ=¤®ŒÕ 2oOxoRo àjµÏ^¥r0²ö*ã%ÿ6p?W Äȶõ À`KWúN´mûO½¦÷8ó(ã<óvlà*ÅbS<t{· DhU÷tS×h ÒSöär¥ Z'7j#²÷’×@ùð3wwQm}{·`p b÷ú.Ö2;6…÷$ÀO<`¸²1-ÏdQVe…Bóîüusà%`> a³—­@tI·8Q׌=ÃU(€÷ï€ro}Ï6^çµ^µ°vtq× cSF \ uû׃ՠXt <óƒKX$€ï<àž– àÕ„_¸&€Q H†o‡sX‡w˜‡{؇ˆƒXˆ‡˜ˆ‹Øˆ‰“X‰—˜‰›Ø‰ŸŠ£XЧ˜Š«ØŠ¯‹³X‹·˜‹!!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏžŧ%£i ‚ÔMSœ ˜JU“(:iS¤#vJyŠ„ƒˆ0¢ÉÔ#ˆH¶̀LÕžaÛêÄ.\ Ã4ˆ°€¯B  ™h "êšh sá% E§‰°™A„6s`r×H^ÕFN‹ùø§™´¾7L±`(•3 aíÛÃÞdÔ(Iu?ñuì‡%µ‹HÅ]ä:ÞÂјOOÜ"z'"¶) Àt"¢wÑ`@€`6DüA ïå'A,ÅÞYÑùõ!à>‰Ð€Vþ4¥ xǶ“†Z-À øwÀ…HèwEd`xàyîB²8Äbq†Á}°!gÄXà¾ÇZ¨šNÀ³A :˜USm âVd{5¨|·„ŒQ™B@çY 8 rGh°™4ÀªA0(p@ƒCäwˆ½±7&P °€Œ•¥ˆbÁ xÀ£D¼— („ (l†£ÙÝY*t±Ùã%qjª˜Ž%¦¬1f ™V¥ÀZŸ†«¨©‚ !c€ J¨˦cA{h¢‹÷¡þ(&wÝ6 .®h:$ÕTcà&¬9F„¡|a' çÉ¡“ªå×@ÚaVÚ5:*ƒ!^áO:Á °z_Æ F*„£ :("úš€Áfq ±-‘±¦|ÄT‹-ƘcÍ ”Æ{lÂÿ ÈÊM,D0ÀðeB<±¤ñ!,ñÂ&¤¥´ ù9=´eBb|¶fL*×MÇÛPBlãqkPDv6m+ €ÁÚ™C:]ÜÃŽ<õ×g á¤`MY,ôu5‘YW ]ã&˜Ên†y ¡Y®ËÁ¬˜¬ËÊ—èD nBþåC„°¸N!:nÕì$þõØÕJ¸†‹˜» ˆ !»¿‹¯g<ƒ»« k^Ó€ÝDà]D~{#Á%|y±I`¾Îƒ`ÂàVÂâ$áùƒÕš@éDø¹qæÕKÿÔÂZÔpÀtðœ`rf>Ñ„G~7zþ~¤ºØìåV“L –½ÌçvÕ‚XÎǘô‰H€Bààú"8ú˜Œ0$‚XÆ'½…è$zFÈWøµ=ý0Ÿ4Óº©|G*ãcÃ8G-ŒÕr>D›˦¹­9x¶ºŸxL&'`Ï5ëÜô@zÐ:QdO¸Á3²¥2Lâ þ«%•±´î;lQ¡fÆ'à '5ªáCÞ€$ø)fB˜S»Ç70€†Dˆ $…`Á†:!‚|¸¡':q‚.Ú¤räLa“£‚Ku¼!d…·BÊè¸\Ð`«Â!×gK ƒÃã oiŠl…“ÌÜpTXÉYú±y,¤ ’Ÿ¶hqÅ¬ÏøþÕ„lL‚ð’@;†…ÈvÁÑÉè´Àn±Â }c»;ánÎ˥旹AFÖL-;ÇÂ#¤E˜‹•˜âÖ¯çqr™\ž99Îk3¢½#t*E@Nó !Àœ4“2Ì-‘èkþÉ0FnO`Öê_BÀ™“–ÀMDƒZ1c"ú@4%8Ú3Si?QšÚ|(4«U­8L¨Ç#(‘×JWZæ¤ðlDÌ¥©ýÃUÒj¥0ÐÑi xzLš€˜7•ÔYŸæ°ÊôTŸ©DéG Â3 ŠGM#Òm0×@L§&àÀ`W* 8n2Î#l`,®ä2„hA(.p(û‰yìǬoi*ªy%-“pZ%æÕ@Å”Àš¦½&6Ø”@)ž‚öU p-6U,D!‹H‚gµ¨uá²¾Õ,g3g+aòÎX¡} æþÆp'º{Õ‚ÞòŸ¨ $p€V,44 @qY€ïJ¬øg]m«JI±) !p€ä:m‰I:ààðSrí„âv4_:ͳþ" [¼*e n gèbâÞÊgcÁf`!Ì}¯>)»¨¥%5‰£ÃqË‚ Ð`¹šÀ»*ó1 ®„¡Í†é¶HN |ˆe 0€ xàh÷2‰x÷LW”y T’òôÔÔ©Z ï5AhS2f¸­…G®šá°€€¡ÏDXŒ?rbD@ŸÊ‹L÷ì†%ÃÒq(dš,CÂOŽ|Ù?Wʣݮþ8·0ˆ„Žž7Mj˜d§¡D@pD]êV›iH@¤8là ȵ®wÍë^ûú×À¶°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØÎv³?°=Iƒ´œ““Ÿ]„—ñÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~ûû߸ÀÕ½_h6“óœ£fÝ:жÄ'Nq_# âÏx¶/®ñŽ{üÙÿ¸ÈGNì—…+c®?€kåµá]îN~Ô…ccÑÔÀù4tŽy•iò ßÁçÙày4” ¦;éð€ú5œî ªþ7Ãê˺;´^ ¬/ÃëÊ;2¸Î²O£àÔ@ûÙ³avu´ÝÕpÄÛÑ1÷¸Ûu7GÞïÎw;ìï»àáxq~ðˆWÃáÁ±øÄ;ž ÷FäOy/Lž—¯¼æ±ymt~ó —Âç±1úЛž ¥·FêOÏz#¬ž¯o½ìc/ ÚËþô¶‡FîoúÝ?Ýæ¼¾|ß â ßñÆÏ:ðÏü'$_Ïo>ߣ?öåKÿúH þ1´ýVs¿ßïþ¦Ã? ò‹_ÍæFúÏ¿×õÿÂýì$ü{1ÿø«­þ»À¿ýÛ£ÿ\ôÿPñ· €hX X€3‘€³Àþ€  ø€-1¯`¨Ø ˜'Ñ«‚H"˜ %8‚!q‚§ ‚(è,X /Ø‚ƒ£@ƒ2ˆ6 9xƒ±ƒŸàƒ<(@Ø C„Q„›€„FÈJ˜ M¸„ ñ„— …PhTX WX…‘…“À…Z^ aø…ý0†`†d¨hØk˜†÷І‹‡nHr˜u8‡ñp‡‡ ‡x¸uÖWv؇vLJ…@ˆ‚èvˆˆ?wˆqgˆƒàˆŒ¨w‰˜‰€7‰t‡‰–¸g•¸‰Œ§‰’¸ˆ 8~¢XŸXŠÝŠ}ÀŠªèy§x‰¤øŠRæŠ{þ`‹´¨z±8¸˜‹°·‹†Œ¾H½ˆÅ8Œº'Œ¡8‹ÈXCÇèwÊØŒ’GÒ¸€ÖˆyÙx¸°ÈŒÜÈÞÈvãŽ+ñŒu€Žæh ê8í¸ŽåWŽ×ðŽð¨~ò¨‹àXÓ˜£¨÷wÕ@þHù‹ü81!o é9 ÙøµG‘©Éy‘ h‘ÉxyŽù ’°P’i€’&É#ù{ ¹’Ø’Å'“0y„4©|/Y“$x“ÐÇ“:©*yAù“¦0”e`”DYƒ>Y}9™””c•NùƒK¹}U9•!•a •X™„W ~þ_Ù•þÀ•_@–b9…aMy–8˜–Â`–lI pÉs—bè–ö¸–vIu©}¹—lˆ—À𗀇‚9 %@‘påÖ:ÞF˜…i‡‡) qnºäE €k 0>™{8™¤ð>˜)~ŽŸ š…(š£@VÔ˜¨Éšª©ˆØàšŒ™?ƒÂ* pRBš³)w² ®Yš\#ðà¼é›¿é‰Á à KÌÙœ~ЗÑé›UÕix÷œŸ°d·™ÛIƒ ž  vÞ™ञ&t ÙÉñAàÙžËh À~ÒÀ‡ÃB €à œÜyþŸøYz9 ó&u7¢q,õ1Uâz º ÚØ «˜¡úyè¡Jz"* Ý9¢tp¢ÎW¢(j!Ê¡-:–,z’3£ù¢6*ê;š£Y£®Ð£>ªx@Ê’0:¤ø ¤(V¤HÊ”8Ú¤ ¡¤J ¥P•L‚WZ¥jù¤ZzTš}YÚ¥ƒ¦¨ð¥bºdº‚iz¦ù·¦0è¦lŠ fŠ¡G§í0§ Z§vºxjžpº§´Ð§A£§€š‰„:Z¨˜¨:ȨФŽJ•‡ú¨¼©ž ¨”Ê£–J„›š©j:©Ù©žú¦ ê¢£š˜Zs¥zªÛª©ÊªG'ªšþ𪰚§\Z«óàª²Š«œºª髼jªï@«Áª«À¬¿z«Èê‡ÇJ’»º¬˜`¬ÐuÏŠ–Í:­Ì ­Øz§Õj Äʫں­|Ú­XH®âz†æÚ…éz®Œ®ìzîú®¨¸®wy­òJ ñz¯Áh¯ÙJ¯úºšüŠ“ÿ:¯Û“;°¼¯Û¡ë¤  û° J­ +±¶±‹¨ËŽþš±ß¹±` ²K£"»¥#Ûªëߊ«{²ÉJ±.;±Ãš²1›Ž4Û®7[³rв:;“%û–9Û³ ´’ù³BKªÊz´Îj´cÊ´JÛ¨Në +[«<û´Vµ‰µVË þU»µøJ´ˆ0µ°Úµ^ ´ZÛ¦g[¶Öš´j{µlÛ¶!û¶pk²3›¶s+ d{·h+·zû~`švÛ·*û·† ¶¬š·‚;‘«¸‰·0Û¸tˬ¹Üº¸“ ¸|{¹ŒK­çlg¦¹\K¸«‰o– ºDZº„PÐ °ÐUV:0»´[»¶{»@¢+¸ïHšêã,e¥`Æ{¼È›¼Êk°»}ûŽ)¢> §~yP¼Ë›½ÊÛ¼¨kºBé¼Q سp MfÐ1Ø«½ê˼à{·ï˜eÅ´p`¾Gy½ë»¾Ü뽤ðŽé"FêsÕK¼÷«¾ù«þ¿¢ðŽð>! ib€¾¬½lÀ ðŽ%P‘QX)i¿¼¼lÁ—Ú¾Q°° Àž[éÁœ¼!,¡۽&ÊÂ-|¼/ óJ Á5Œ¼7ŒÃѪÃMðaEÜ4ÜÃ? ÄÞ*ÄLðŰYàRe™Ä5¼ÄL,—N$€%L˜•òšEPt4†:<ÜÃÆ«Å[Œ·]\œ¤c(Ĺe]æ<’+i¬ÆlÜÆ°‘åû¶™2ò^[æpA“¾|üÆ[›‘%p_‚LÆChC@n‰¬Æ6ÌÈVûŽ €¼*\Æ0žB ³F'{¬Äšü´ïþèÉŒž@sJ0ŸFj•Q—ŒÉì+Ã~œØ)ÉbjËå§ŠÌʾüËWÌÄ)kD iÆÌ˽¬ÌkË ,wÍà׬͂¦|dãf\C PÎ pÌ÷ÛÇÔlñ¹ ëÉÄ™`È — èÜÂê¼ÎEÛ ðÏÙœÍÍÍL°ŸýùŸ T\æe3‡ÏÒ¼ÏüŒ¹Õð T¡D'_W‚ÆX¬Ï­¬´ºšÏ Ñ °Á@‹ÅÃ!À¼,Ò#ýˆ]Z‘›¬B``°ÊYÓBûŽ>p¦ºÀ›,É.ýÒðŽ06'nÁÒ:ݳïû Èß Òþ\ÔF½ÃQí8³Qú¢8d½X=ÀZ½Õ­ØÕ&P^&p€ÊH\Öé¬Ö1ûŽ 0A5 0h–÷ÔYm×.ûŽ6åmõK×ø+Ø'ûŽ”†}Õ­Ø#ûŽ\0<ÝÒ’í±‡6(»NؼÙË% sc1בÌhz›-¬­Ù®ýÚS*ت 1Ø2i7 ØfmÚûŽë5ŸûפMÁÂ-±Œ- Ô™MÔÍý°ï¸ÀC»ÜÙ{Ö¸ÝÚÝm˜·Í’Ú ÂÕ½°ßmÛá­éMÝã½Þ~úÞXZÞÛë‘%mÉÝÝí½ÈÇÚ1¤ ß¹-þߪ€ÓÝß׋»Ž»º+àÃ}ÞÓÍßN]à à`Êàe Üumà®Þ«®¦ôíÂÙ1îÞîá!â><âPâ~â¶J­*žÉ>á0>¨)nãWìâÈ|ãñãþÛ<žÓŽÞ~¾žØ5ä>¾ß=¾ÂC^à>Žã2®ãåP]äëäDáVã\.åPþå'æYîåL~ãfØKnâS®ª@îæBNæ¾æÁÝæ/>åvžáh.çM~ä‡Mç~Ùâ‚à{®ä}žç®åû›ä¥çO®ç€NÖ…Îy„žæ`>éC­èW^éë}èžè‘¾èqÎéhŠålþæpþ^å˜N——îçj®é£íéÃ÷ê¦^æ²¾Â3¾Æ,Žêw®ê ÎÜÞåoìÛÝ봮߹þÛ»>ÍcÞêu¾ìWÜìÜ­©¾ÎçÅ.íÊìT@âÜþÚÆnÞÃ.æ’ÎèJIíÈíîõ=îgžíæ~ÀŽ.ì¢Nìå^ê£ÞéênèÚŽ¦èîî©ïønï;þíhÍî"ð¿.ð¬ë§nð[ð+®ðØ~ï ë[àíûßOãõNî¤~ñùþðÿéýîêÿþñïnñ3›òÏîð¸ïêòs^òáÝñ¼NñˆÎð-ñP ñ0í2Áó~ì:ê<ï‡4_ð6¯ìCO•K¯ïA¿îþ'ŸñE/î*ð,¯ô>¿¢×¾ó[Ï­QOòSÏïO?Âcïê_ôaˆioõkOïI/ö]oíuÑ8ïì5_ö_õƒþöƒ÷Fì~ÌWßîY¿ðm?®€oø‚õsïöwÛøÒß;]ø–Þø–^ù ïÔÿ-Õš¿Ì‡ÿù/ñŸàªO» žùgÏ©œOúž?ñ‰_ñ5›÷ÕÞ@úŽ?ùâŠûéÎ÷²ïûÛ üG/÷{Ïû þú¡ûµ>ûú#¿Ø£_ëÎ_»?ýpOüØjüµöL/üÖ]ýØ_ú´/ý/õÊïÜäßíæýɯýÏýÓêýèòê/ÿ§ÝþS@þà+þ@`‰Eb2-™Mçú*FëÕ‚TF¹]ïÉeóV¯Ùm÷O·rûŸïN)XÿP oªêÏ/ðn°Ð0I¯Ññ2Rr’²Ò’ŒîRs OñêÐ.ÔJTŽ´´è”³Õõ6VvV/“öö–OÕˆ.uW¨÷íX÷9Yy™ù̶R±“:Ëz”*;x;:\|œ¼¼Ü<=m:Û¸x× ^U^ý?_?ùy¹€8DÙaBNØQ³§†^©‡ëºy›ècFç ã8k€ ØE >€Q‚¡'oÚê ªØÎã°þ›s~ôùhP)=…^ê`’ ƒ(‰šhX¬éšˆ .:Û µfQ­[¹Þë×ÕÒM~xr0kÌO8ÓêÄÆ³­›©Šª‚µ{說y!8Ð$€ OR  Aˆ¡ká º/*ÄÇõ"óµ|sǸ™í`ÐdD O<Ÿ¿ ü¶cr{ê Gœru/”qš÷îŸ+ÿ<œË Íü@x!ß[rÎA_Ñù$þê´õ^zsY¿×ó„ÝqòN§=uÛqþÝñä½v¡ƒWžøæg1>Ùº'¿o·lvûV ù彦ÞóìÅ×3vºã4èëóþ]ëñÝ׫|À¥W=ýh×WÿýüÙžÍî;Gÿ~öÓß;Ñ;ÌÍOxà õH@êÄ€£C ó8½úqë|Gü7Áï]ða Ä IH¤¾Îƒoóž ÿgÁ¾@<àù\(ÂÊevíƒácxÂÝ¥0r-¤_GÈC#F͇Ç"ꄘ@‚íˆQŒI£GÃ!ÚŠ8dßÙ¤ØEÙQ‘{K^)øÄêy1Šü[“ÿ*xE-âþGT£šØhÆð½±qäá›TG"Þpƒ[œ½ÈGj‰Q‡düà³HH2I~Äâ GG¾’H’$‹XÉ<^’„™tÑ&=ÙIزq L£ %hE'2’’§´¤*3(Êo!’‹+ b_IËP²…®,#,ïXJ@úÒ¶T)eùÉ>¡RsÈ|$0(ÌEN²˜Í4¥4õ§Ì1ó™³ §3¹ù>oZœ’fé¶Nj–3lçT.©Hòr˜ð <”N ‰SÕgöøÉ É'T[*úÀ‚Öç Ñ(B'ÚPâ=T>SB“wM‹Ž£ñÑh 9ªK&~4þ!…ÏH›SR{î¥îS©vXÊžæò¥')HßYÅŠnô§$ êN+7Ӄѓ¡9#>=JÔ‹ö4ŒÖ¼§ÈÉc:õvFµNMq5T›º«ØÓju¸j:¯v•œaýÜX©SÖvžÕ¬iU+åØê·– ¬T­á\YWW‰!5šJM$S§Ê׬Bµ€e'^áúV¹–o~ÕÍ]SvÓz¦]d?¦X`.4°˜Õ©fAÇYÜP¶fªjI[5ÓrµM³lRE»ÔÖ®±kô¬BgZ½º± %pÉp‰[\ã&·¥cn—+ÕÌb³ªÆl$lR]ë^»`T®Õ˜ÛÇÝv´°þÐÝë)Póž½éUo(ÐÝíÎǽ‘ü®ImKÌèjóªï…h|59_Áâ´¶ƒým/õ;OíêÖ¹£/pñ;Ýqn³À"åï(ý`8ŸVup„W:á[&¸¾ &°tcùàürø¨nncËÚ–²¸²yE±A=¼Ì c¸© .q@]<ãÓÖø›7¶ïxuœMoØÇm2:…,â “ØÈ<†p’íºäy‚XÀCfð‘wüÏSù2¯ÍLl¹ãÔN™¢_3_ÄŒ2ËNÆNÎ1—£¼æÎªØ»X¾°–G\äûÒùÏv¾sÇÞÜ63Ë6Îî² Çlå~6YÑuN3š%ªfFƒ¥Íaþ†4”íeJ¯öÓ—Ö—£i¬çËâ8¼‘îô¤O,jËdúÕ›ö3‘Mk)·ÚÕ£Æó!MM[T?WÕ¶ö4®s}4Rï·×¾åó“g½å[#¹Ø;;vFe]kgÚ•u´¹k6WûÙ‹Ö6±…jinÅÛ£Nöbc|è2›»Ýð>÷GÒ-íu¶·ì>3¹_,ïyûmÚ¾7oÍia³:ÛÿÖJ½=pðÛà×F¸¸^†Û¥Ðs7œ7nhWÜÇ4¸±Mñroû«9>D.€„ \ X€ÈÎáôͲœSñ>¯|+-ŒH‚'`ðpó¡­Pþ«ÔKu cDèÐÀ6… X£@€¬¬ï²ï[[3 ¶Äý¡öJ¹n»æË}šÀ˜Å 0@0P¥§ó;åçø¬‚³öŸk¤ðÁ=îâï’ä%ñqOﮔۤ ˆ¹œn)’O\ÒãÖVä­}ø4·ñ©'îãí@zÉ`ö±x$\@¹çÅ3 !Î&|SVÎÿ{êe¯ö _µUdßüêú0ùV)ïz©¯ÞöÞÞÑÿ!öï݇ %èýïƒÿ÷„ŸðQ–öÑþæL¿úñg/5Ùßï³£þ—@½êUÏúâÍþ'ü0ã àC–à(:oþ €O(À#P)`L‚¸<`2P7;P=€¸b&'°7à=PW‡KKð‰ VÐ9°]âcgp¸0ðƒ0A`y}Ð%€0mpAà–ð‡°P‘” =° ±0 q°¸¸° ?°¸j0 ;p ËÐ CÐùÖÐ ½0ßP ]ðîðƒ ìöŽ úÎJ˜à Ø ÑQ‘Ñ#Q'‘+Ñ/3Qaîòdë4„Cœàëð.ì*¯ H`Z‚ÿV‘[Ñ_cQgþ‘kÑosQw‘{у1?`"Q`ÄJbFîÃJ®”Žéöðê¦1_ž$J`JÀï–€ JlŽÃQÇ‘ËÑ ëI`Nøøø¬‹ûŠâIB$€!@KÀKÔ$`›@«Ë›Ä K€jì9ò±[ˆN ňC?ÀÿÔD`Jâšà8lŽÎ•ä"36’ îƒ%\b5š¤`$8àšn B æ<à# $‘åM ë %"בPH $›  ¹B#ÞD(Mr — "]$'K#›Ò(—à8’rZè.þëP€(ùd)Ãoü¸. Õd,™À)'EïL@ü|Ï,»E½ƒ %"£$@ñ$-M@êr " ¢R*‡R-·r’MH@óþó®d0§Åû˜@(Áow"à44À8/( ÓÀ2ü±1AÓ2< 4‚0“¤@ Dó·Y `¾¯RH`º#OúR6—€4פ/ŸÀî"SM$ .ÓúŽ1y…;Ð8  5•¤/!³ ³:Sd8yÏÞävo&S2·ST¸r ôSîÃ< “)ß² Ú²4™ÒIƒM“>M ,5ÄÚTvR)0E7y“/M³þ(2´R8MÓ ŠsM“à@±2"tZÄC’±ü Å$€P£3M:€RÂ8€`¢#m$ׄDS¢P´FÀ%= ³?Õ¤ï²%>ï:²: E» ¢$æ b21ൢ®ËJò²Iž´º`H  À$À?ëc°ËRÏMÓTM×”MÛÔMßNãTNç”NëÔNïOóTO÷”OûÔ©6 K €þ²¡à:ÀO•H  %F I¹@¸š àUS-A9É LÄSÇ ¾sSKJ"!9™À …t Üþî`K$ %‘dõQëÀ)CÀÇ àbr,ô/1ิþR–9Cà5àŒÑºnÓT«õDàë0B <™ À”nPŽ& ¢”ÂïJ ï€De5²RóæBb2À¦.ÿr GD`8f$< àtS±#W·ÕZV~’Y u ¢._3$;ÓKB.ÝRHUÝŽBu îÃKþò, µaÁï™ `–eá€aM [— LÑ 84 ÂÓF¾î%ÜÒ¼±)©³ d•üNâ/Ô/.<òëŒqe[j×@6WUf¨þ2• ¾ÎFúŽLö)ù“# °XÇvYM9C²V—ài£ÖmÍÀ C¨Ö rh+Ïns¼äë:À¸B6lk6l#ò$†3Mf@m·±mßÖqÃàeé6o—àj—€b¡2Kl¤l¡`-‡@–JlÄpͶaCò ¬gWu¿àäv bÖr  òÊ•ÎõI­¤ï G8 L=5Dêõ^ _Ås þµaomkvüv%WWz› r6v­–ZCSV úîñ:@! Ðé‚·fUX‰y“`— š7d²wzç· ä³ ì—~ó÷ @é8`P϶ ~UJØ"€Kà Ñ ˜Ø‚#X‚'˜‚+Ø‚/ƒ3Xƒ7˜ƒ;؃?„CX„G˜„KØ„O…SX…W˜…[Ø…_†ó7!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏžŧ%£i ‚ÔMSœ ˜Ju©’`@‰"` Á‡@H›"±SÊS4 Øj ˆ&S "ÉÛ2 0m{æí_¨¸pABc Kx Âú y€™rƒ6*!⮉¦¤¿ž& …´é4`!Âå˜ä5²÷µ‘Ãcþi†-q ÇS, ¸u „ÙPm«]J”1Ž„»èBáú¬’ÝE¤ú.âpähÚÃWnñ}Áß ÈV. 4 Õ¬—X‚}ÖIPÄØ¥ÅšNDù€€Dþø$Bc}ÐTƒé5·š޵€x$< Õ"¡Sâ‘,½ùµSŸiã ¡h7Ü ’Õ*hß[ŽEàk¦(YnD˜  ZUS‰æa“jŸ|7`(¢˜¦©«TÛëþØ6ªms,jcˆ'­«´‚æëš IEfæ-AeF>-àY~&áÁ†m‡8¾¶§P€j% `±¤¨>¸*aja,Ú‰ .…ç¡ Û)“м$ô& Þqt³g-­#B­ähÖoŒÛc›6{)ÀßȦF]Â‘Ž¢™îÑsC¨•h8ãÉÕ|,9:¼gÖv)„z‚09…¤Þä®þ§P3R’¢ã$H&‚fÉL=ÍŸLd^-\C¸ZvJ„…©& ,BÈ’Ó˜çiB .c"j À4%I Zî'CJjjÆ‚1­þP½´ c€œ’ýBB1÷òDˆyÕ ØŠÓ,†1DÝ4œÑDƨúÃ¥¶õoæ¡d²–6©rm˜LæYMÒ˜Äh hÔgò­ ³y©¤Ôç*R Њðø©Ø4Á@C0€+èb ©Õ]@·È IÛ#!±¼Ê\ *èq~K 7Rµ¬#™„Øä§6· ¥\aêA‹‚°U (×§† þ¬F5kqÑ­dº˜©~a²©ím)È3V’3Ëâ­Z5æÇˆç¥–傜› è $pÀXü&4A[YÀäPç]„À2‚À’ô· i+ Ý\k ¬xÐ W '/I‰x˜Ë"P‚†‚ñŸšÒ‚ã¤Ü"Ä8AXI‡ÀÐ_!.s!‹]ðÂÝ<æS"¸­ö¾#qx²•5‹ÁôÀäàMÅ`qt³Œ(I2ºm™9„,ø @@>ÅÈ®À< µ¸–D­{ˆí+ñÍkøÓ$Ñ›€·)´F-5í"ú 0'2M„þ9=ù#oôHÐ_aíÒr A¹ ^MÄ i(Gø¬ÉœÕm¨–£![³ä×À® t:J„§b–¶²U[·š¯. 6ðXûÚØÎ¶¶·Íín{ûÛà·¸ÇMîr›ûÜèN·º×Íîv»ûÝðŽ·¼çMïzÛ[ÝØQ¡ñÛÓÓCðô,­Hð‚üàO¸ÂÎð†;üá¸Ä'NñŠ[üâϸÆ7ÎñŽ{üã ·Ý„ùÍJ#@ëFÆótYš{»üå0ß6bNóšÛ{æ6ϹÎÙóûüçᆯŸŸ1'TûÕÞLÿì”ç tà3CÇÏ®«‘Tªþk£êÓ:5vfÅã½&Á;>‚Ö±õi”]g‡ÆØß±vk¤oFÜ›ÑövÔsoFÞ™±weÜ}—ÆÈ©1øi>OGâ—Íø@,þo¼ä÷ùrT~ò˜¯ÃåDZùÌ{þ Gè?Oú4Œþ§/½êÇún´~õ°ïÂë·1ûØÛþ µÏFîoÏû(ìþ¿ï½ð—üjøÈ?2¦ÎŽã'ÿùΗFôŸ?üé«}ùÔϾ¬ÿ îk?öÞ§;ö¿Oþ)„Ÿç/ÿçÓï÷ñ«ÿýL`2äÿÆÓÿ÷¯¿²ó_ þë×þ7 øˆ6€Á`€hYø ˜þ€–Ô€½(7¸ 8ôq¹ x Ø6‚µ@‚"8&8 )x‚0±‚±à‚,Ø0ø 3ƒ*Qƒ­€ƒ6x:¸ =¸ƒ$ñƒ© „@Dx GX„‘„¥À„J¸N8 Qø„1…¡`…TXXø [˜…Ñ…†^øb¸ e8† q†™ †h˜lx o؆‡•@‡r8v8 yx‡±‡‘à‡|Ø€øƒˆúPˆ€ˆ†xЏ¸ˆôðˆ‰ ‰”x—X‰î‰…À‰šxî·‰¡ø‰“牃`Фy£Ø|«˜ŠË†ŠŽ×Š®l°µþ8‹¢'‹Š§‹¸xi·è¿Ø‹®Ç‹ªÈ|ˆjÁÈÉxŒºGŒæ°ŒÌ|ÎhyÓÞy€Ö˜uÕÈyݸø¹hŒà8NÚxçXŽÎŽš'ŽêxìHñøŽó玨gôˆ‚ø8Œä˜k2rþH  9¸ÜpÉ€ I{Ù7‘ÍØ ‰ y‘˜‘Òh‘IÉ#’ è‘ÖP’&Y‚(i|-¹’Kø’Ü’0ù*iz2Y“q“p‘“:Y…>y}4ù“+Á“g`”DéƒAÙ}K™”_Ø”â7”NɃP‰~U9• ”e •XÙ„WÙ~þRÙ•#Á•¬÷•b‰d)iy–\h–Ȱ–l†n‰s——_€—v¹†uÙ}¹—þ —²÷—€É‚ „Y˜‡˜˜ –Š™‡¹‘ù˜0™Y`™”™ˆŒ ˜™™Ž¸™阞I‚Y %wðe;;Õ™£‰‚)®Âþ6„¢ Pm 0?®ùš†0™˜T›B@(Ù›¾I“IidðUœ yœv÷œ¤ œëT„²Ð9Å4¢ ‘œËY ð ÛiœÞi‹Ò9 Ê)œ¿±jÊ×ë¹à ŸFp[óYŸ‘œPJ ŸD€ÀʇÇþŸ!OjC×ðžN ò9 í© äp˜ÀKÄ9ÞµÀ€蹟 ”ô9 úB&wÁ¦Á,Bju¥ž'šª 4Z£èx£È£:Ê–è£? ‘) ¤C:9 Iz¤q°¤Nà¤LêPB¥)Y¥,Y¤VºSJ|Xº¥Ò÷¥*(¦`Ê”ZJ¤eš•dú‚kš¦ËÐ¥J§nZ–gª r:§ap§bצxJ—uZ‘}Š–|ê z¨ˆù§9¨†* …j¢‹Š‡ŠÊ ú¨¸©J‰¨”º‹˜z¥›š©Ïh©ª0©žj~ :„¥:ª'Ù©Ô ª¨ª¤§Š„¯Úªcþªª3)«‚«^I«¶j¤l‡«»J¨¾*…Áú«—¤ºJ¬€ ¬Š¬H°¬ËʬFà¬Ã ­¦ ­ÇJ­«:­ ð¬ØÊÆÚ­ó`­à*â:®Êª­my­æºŽèê Üڭ庮ѩ®ìJ¯ò –ßz¯¬h¯Vɯúj ñú¯è°û©þН;°í*—›°œ¹°œð®ØJ°+[±àp±ë »± ±f²Ë;²ÉÚ« k²¸P²*Ë©ùÚ²¼*Š) ³´À²4¦3 °"{³È¹³˜ ±Ôj³¼}n,%ð`rž›¦i—Ç{ÜÇŽÌÆƒ<·¹°–Ü*ÕÓsŒÙ mÃÄ4üÈ|üÇü³‘¬•ŒªÌÅC§þн†F¡DÆ¢ìȤ\ÊZ{ÊaðÊAó¼”39´\Ë£¬Ëj{ŸI0®öÉx¥ÇÂ<Ì€ŒËVÀŽH7ÍàHgÍ­Œ`ÿ™÷Y¶Ì✡\Ë· ÍŽÀ ê »p‡ Ÿ)wÇÊWÎÍlÆçŒÎ2Ì ÐÏÕüÏýlÍM¡Ú¡ÔƒUMÆàgÁ|ÏhœÏú¹Ï\ +J-ZaI #KlÏ÷ Ñ͹AÊÑÍìÑý™ý$ð,]Ã=)ÒÂLÒ%ýº'Ý#0×1[ôÒÆ2=Ó˜HÌ>?Ëç»°e Óæ,Ôc ã'Œ–½J-Ê? ÔþÈÔþqÃÇ[Õ|ÕXm¾5ÍP'Ð=_ÝÓ­ÕW ýe€ÜÖnÏp-µ ¹‘US±Ÿæ íÖb=Ö MµoI½×|]ÖŠ½Ä’­pàØ[ÉÌØ“ Œ}-pàNî Ö¶ÚI[!0j×!ÄÛ¨ÝÇžýÙ L %ðt ÁÓ­ÚF»‘"‚ÃÛ{]Û¶­é»ã-°–—œ]ܾ=´éæ‚Ù0ìܳíÌÇÔ•˜H­×ÐÝݶ zÏØÑͳҚÝklÜÛM¶! ÙLÞŸÝÞô ßñÝŽö «îíÇë}þ³øÝÛý­Ø>Þûݳî•ÿíÓN³®Þת>ívÝ“á=­ß¾wq ¾±îÐîª0Å$>ÅUÜáûá”øQùýàõ©â#ÍâàâžàŽ*Š þÖ×Úâ/Žâ+ã1Mã6Žà8žÇ@~…éáDþãGŽäó}ã-\äþäÞz®;~Æ!þ>.åO.äKÝã5îäVæVÝä^~äfÖhnä_ㇻä ÞæU^æp®‡ÍäbNåznåËœäÛ*ç+¾çdþæ€Þ–YÙÅËçs~è»æ©MèiŽãNÛtÞçvîèr™èõ½è…®æwŽ“œ¾åñËèƒîç•®ÝSþþé”êÄ]çlê3®éúšêï}éŽê®.Þ°îܲ>ä´~¯¶à’îæ ì+è³î铞àÃîàÅÞë­Žì|9ê¸~ê™åÆnØcÞìûýì<ÎìÛîì»~Ú¬>˜¿æºNí¦líю醮íÒŽîçþíå¾Ùî.îónïì‡ÊìúïÇ.ïÏíé~æý®à®å×¾ìÙŽåõ.™Ý>îüNð¹¾êÞß ¯èOñïTñ‚|ðlžðð òñ#/òǽñÞñûþñ&O y~ñ±ÎòåòÅ›ï0_ðä>ó„Ëó7¯ò-¯ó²ó•Jò‘¾îíCïñÛíò¤Þþ]õEô’PóNïëHáFÁBÏõDŸóXϸaoð]oà_Ýi¼óQ¿öÜ~öô>ö÷-÷ƒI÷ˆ©ô–^öÒ‹÷ˆ©÷Ï÷ªïo÷Ñüö>_ñ‡oõXPõpõMïðOùd?ùßó6?ðoùIßöX-õ Ÿù†ß«‚¿òˆ_În>²¢ÿî›Ïí<×€/ñ§O»„ëb>â%ÞûN|â´ï÷‘{û¯ø±_÷ޝ²¯ðÇ¿÷ Ÿâµ/ÈÄÿù©ïö¹OìÁùê®ù[öÕïáÑO»ÓŸø×íÌßýÒþÅ?þ÷aüèïüßýÂOºÿ¾ý•¿øƒÿüA®þ•ÊþþÖ¯ÿ@`‰EP±,™MçÓR€ŒUë›Õn¹]ïÉeóV¯Ù`$µ—ÏéY$š‡Jáq¤R/p‰oîOP°Nq‘±Ññ2RrÒëòSîî00Ñ/‰SϳÍ0to*3Uu•µÕõµÑvvvÓôi”­ô¶)wm—w•–¸Øø9MV¹9/˜ÉW 8˜:ÍšÛ¹Ûû<œtX¼ÜÌV:Š|0ûLûöÝœ¾ÞþÞ˜ÿ ]z¾L€Q¢(´ÅäÄ6ŒÛCp¯Öµ{· R¼k:xÂ`X¤Fê èô$Ô”JŸîuü²ÑÉ<@è#@!|pò¹ˆB·E|´3eÕ«ïZe]æÀ"!d0v Bùü¶tÜÓ…S']üurå ]/S€‘0ñ ¡Ã‡ ;{ù=ù—ÑÂWº¸sõë÷5g¿Å@ô!#TÇ""@ƒ´ÑV¦?Ì8±BÎο÷ <Ð÷´Ù† Í¶,¸Ì;µ.¢K¼þÁ+P4 IãpÁElEÁ‡x Ì ›0-x1þÐ@Ôh<DuÜQ‘ ø‘Ê;°/Ÿ„X °,ðÓ¯BÓÄCòr”¬Fâx¼ËKJÜ1«­PüÊ„®F8<À€»ýp¼ñÂ!¹O7³œ“Nɦ ±„àÉÈ0Ó„  €Á7 ûcsQ(«œ±ÎH%­èÎIíPt)F3utMK=ý4ÃJAMÔÉ9mSÔ7£Œ3ÕQ]½rËWûÁôÉO%PÓZeÝÔXyUÕ\MµµÓ_•Ô×c uX‚=µQe¥Å2Ùc¿ûPX¡ÝtÚnu¬ÖØk¥ÌÖþÊmuõ]ÁýU\VÉ…”ØgÓ×Àuym7 Í»uÃwq¥àõìÝßSZÕÎ|åDxU…bš–µ`\¾”_lÍm6âŽ#›øÕŠÐ·‹„ ö·_U† dWEîåb,L¶øa™>yåœñjyÔ—§‰yÖŒÇÝX[¦Šç^™-ÚÙh›æöè¨%úSŸ…©9èbŸ>Wê®AJºê¥Ë×馄v×ë´¿¦ÚS«9"zì­9V›n…Àn[lxåfÚl­ëþ»=¶-u›dUÏve_üž»Ïû_²¡î[^Æ-/ÇñI <­Ãq–œëËEOsd!Oô¹ó{ôÖ“ÉþÜôRù®ÈsšÚõÜ‘=ÒͱéfÛáÖ[÷âk)½÷Óß;nÊË6zVx¯Ó÷ÛÑN}öèµÇdz:«>ræ‰ßžüIºŸó{ì›§õòÝwäü,Ó?|ç'ÿEâ§VyÜéGýËËßé°?XõïzôŸýBG@¦äwx“ÝúVW9ªî<ò¨‡@Ā̞Ihš>n‚ã«àóVx¿¾0T*›Ÿ)Ø’ÚŒƒ0tŸy4CöYð‡,ÔámvBÍyðs L UHD'&ʈ±kŸ §¸Œàá0ŠOÜwäÃ6Њ7„Yµ=.~ ‰ÂS_ÃXÅ2:þñŒ&ò"øE ¾Ž9D_±h½®±~xÔbG4Ç6Ö‘ŽBä )"C‘‡Tä"aØÈ=rƒbü—¥É«Qò–\&ÿãÆLš”¥ºø8F?&‘‰Le%õ(¿Vnò•jTâgÉÈZò/…²L¤ ‡ Æ^fp•"e^<ù6@†ð˜%Lf½nùI 0ˆÄŒæ§ùžev¨™…ÛÂÌú¸MZrrÁ„¦G(Imšs€ÝdÏ7MÎ’Ó•ð”æ/¨Nl²³†î4¦>ß'OUÓ™»„e6JÐòT=ôŽ=9‰O\:™üì!BÅÉ0Šæ²œu D#Q*¡’þ™()ôHº“fè£×\àJ ªÑ.rôžWÌç3ÿISòµT9/Ý—JëITŸº¨ÉjÉtzQžÎô¨[´)ý ÕbÞ’“Œjñ’úš¥Ψ'äV¹:U9â´¢MµæS—HV3šµhéNªK·¾¶¬j[ÊÆ¬¾ó®­ë*k¾:Nµ&4–ë lÂz4¬0ìbéÖXÕ<c“ejL)k<ËR³6ãl]CÚYÝ}öcr•)_¯ÚNÓZµ,S-[y™Øž¾¶puälIKWÛZ·Œ‹ícB›µ±žò¸Á­ÛpS\àö·¬EC ÌR]ë^»ØE‹rјW`j¬þÉM©x;$óž½éõ.w§¶Þîµ¶ Å*rµz T¿ùÕï~ù[ è–½aî^œ[‹®µ·NM0‚ÌJ÷Þ¾ í«0:ßR’·Á^ð(yÝøNX±vm†]ºaVFØ®òñ…ë;Þ“˜°&Vf‡Uп6ÔÅ€…±†LUðº5ö+}u¼cÇʘš(.­‡%ÜZÇF-’½IcßöÆÎq”¥,ÛŸUɾ2…³¼â-k¹Ë^–¡•wZ²¨µTD]—'*È‘–î¤É¼j;/z·fð˜CLë^kÚжFÚ¥ªkÄòËÀ&ô¡?-lV;¢¤.ô²9ÝlOÃÙÙÏÆõ%¥ÍllŸÚÚ©w¶eÒ꫼ºs}­º;nr“ÄÜÃ6vG3«êͲû¹ø~÷BâMçnWûÛ×Þ³ÀM½ï˜ôûÙóÎi¬Û\ëRÛà‡vIÿ ûâCµwÄ;‚ð?Wœà9µ->pŽÄã7Aw…ŽgJ{Ûä'ïøÄK¬ð´¶Ü×É~8Ée.`šƒ$';éÉH`™ À9Æc~ïYß=¯þGÊ¿Ñ%®øÅ%0À6à éÀrwxÇõ§CöìRܶ¥R¤! © †Ö@ª¨§;í뮘þk…ðýS€Û£‚""pš) @ üIìwzÙÑî4ùô]ç ¹ü§6?øI î @„ˆ°"ô%ìžYºÆ)_oË#[Ò é¼¥fo:Âß:Ò©"|}ÂÊUseë­öÓ~ù>’©æ{ïç‰îí3„ôþ÷Á|ÓÉ.ùîëêù%/ø¨›]óŸÿ,t?Ê~÷Ww»!Y?ÿnO“ØÌ¦6F“„€ú"_(@ Ð)`¼Âþº<`Ð#Ð=Àº€b07`%Ð?«Ë3p­ >P!0ÍbKO°ºpk°A(ÒKwPg°mðºàWðsP(8Ї¯‹™Pož YðºR° #P ©0 P ;  ­°Ãp ËPoR E†ñÏñ OEÐïóP÷ûÐÿQ‘ ÑQ‘±oNÜîH’¤äèÎH`Êâý:Ñ?CQG‘KÑOSQW‘[Ñ_cQgþ‘Añ6€(&¥K< +ÀEEë¸ÎëÕÎ&OöäúÄèЊnOî§‘«Ñ¯;+O„NøèøÎ«þ>"O²$ (€PQtd ‰ Í«ñtÄ`P€2Ð1?Ö]¬®%Åêƒ? þxD`ºâˆ?޶.G$!òD€,Ì¢;L¤`+8àÀNBàè< "`"§EMî"e »±NHà"‰ `»¦#®„&1R  …à Ed%K@!'…?vÒ[ Ï Õ°N€O&é¤'­ûÞnw¤*‡(þ-E¯Ï÷°]îïAò¯NrO`Š1K¶ÒDÀ,… `(‰²&¹²P` `%yäúô.I/PèÒ[¦oh²úÐ'²C R¯-ïÒ¤ÒúPtÄ-M 0À®ƒ'êÒ#5r2µo,³0€ú>…ÀAæÄ-)³4ÑD2³O0uDÌ6…àñúòXÿnSR @3%30‰`.Ah³À®$7«B”“0»Å)…à AE˜sDÜ,ã0+Ó'³.?xä$S®òH À;e¥%TV³5#³@ñ8ÔCU2]rbO†NR0`$õ²Â$ ½%øQyHÍ+F€  @Þ“= ½<ƒHý1³TK·”K»ÔK¿LÃTLÇ”LËÔLÏMÓTM×”MÛÔMj`.!± . Þ”O3€Êb|´ ¨Ë7«  Ou(“9¥4 •$u=£“Q/µºB( 7‡;`$ ð>`$@Ââ29eú(‡þôèL €T å@á. I;.5@r3 P Ï 51Yå@ä@ Òs `DÕèTñL²':0 ¯ï:tU»( €3“.+ <€8Ã.`ÌDèLÒÓ.àLuø•#“•_Û &µN…`'€QÕ„u"Q¦#,M  å'±N„dO…E.ÇóN¶úØpðµ_Cv þÕ U ô&V>LoÒsL,Q(–Bšñ W5û¾.÷ó-±.=ýQîrdEVhÍ6;õd7Ž5h5ò¯JÃ$(ÛÓ!óW§ÖWM 7þ'U×Ó`‡ÖkÅÀÀHŒÖð‘ ô`óz–i;»~2je5jò+è#17¶d@képi¿Öo¹€dÉÖl‡ i…a…rPƤj« +Ï*ÖOÆÄn­`'(…eÿVsµàÄVLÖrpC[9@[PÌ„"#³óH]ÕØu÷æër3þÂb`ܶ#7Wx‰ p6t‘öX'sU /þ:€÷/ì`WViµToõv•’gõv¶ÖB`U y‡—|Ñ@Ãà|ËW}YºŽèôjÁ`V×—~U!š4„p Ò·~û×ÿ€X3€˜€ Ø€X˜Ø‚#X‚'˜‚+Ø‚/ƒ3Xƒ7˜ƒ;؃?„Cx}ƒ!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbbkkkttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›³Dx0D'þO>{p %(%£i‚ÔMSœ˜Ju’ T³N5â¡€tøƒÔhÙEÑ®ñ`@"Œh²ÕˆTHžšQÊTí™§z¡R à‚„ ˆ1 Ø0≠ DøSB ¡?;îûJç¹j(`!Â80©[D*j#Éðê×̈޼K ¶X`)ÆѰ³çdrÿ¾çBåD8ða m"Ü.’{ÌnDÑL'|øÄÝRBÝD ô¯Ô?¢ÛŒ6D€A ðy€µYeÂoéGh!0@x@„O"þxÀF)xAƒ uøÕØ‘ðÀWh˜„N`WsìgÄwCØ–×N"0ðU†Fl€À…`Pßt&p€rDh ‚Æ=ÕßW èÓ.i„“8€ƒhÀÁ%©¥{}©¸èŒ8DlÖt‡D ¸\I0€40ßmpvA|9„ˆE7á¤g- À˜m'[_CjöF}°`¤Æñ†€ €!0 â…D˜  wJÀvDxãmÀf:&”)jÀ¯Ðéd)¯nç¦ ‰B°h£Qö&(¡ Ú—¥šÁ À³Ì'-£»}§þˆI€ÝoË6û-šI•Uz.QÂ…ãI€ÇR…€ŸDxp€Ÿ,`£Pܦ`­&ˆZÀh% W*¤Dœç—QëõUç ¬šìT ‚&Å9–ªŠ a] ÕfH § œkœ‡Ióv½ µ± ¼5Ú` ˜mÊ'Ëi’ á0ÄOÊ›.¬±Ò 6Ý0T qe= Ê^œþ ¯C|@B !ÀL`¥µápÀlC¶@ü+'A“Bê\¸dá°âdW\mif %”û…;3où ‘±¨!!i_¥>[}÷îıÏ @ËIˆ™U=÷Eùþ% N7— !Ÿq¿…naCØÎ8ï~ÁI¸ †óæ;ð·'î4ØBd,Ôòœ=e +ÜñáÂN»TõÁ%ô}0¯éœ³ÈÁ}€÷´CoÉB|ŽTÆ•ÎøóosK8ã%Jö죤Ú ¨Ce~â¡GõÏŽL粟½FV9€st2@ iÇϽ„”×D g†iŸè:ˆœ!¼¦W`™¯XëID'ÕS‚|¤:!\€J @¾%ôËuÝ P*Q)MƒœZΡ‘MQr¾ú‰kS*é Álã¡ “»"Ô,eZÑ ²§ŠKS‹þ‹€¼<¥…B8ÖB×M¥;w@÷(ÃB 6| t¥8=QÌéeR†„ pJxÍù„pA"”@~œ+Bª¡È•Œ„`Ô¢ Î a”†4*ëç;"Ð RI]Öè7úŠ‚´`k£Aÿ ‘¤] ©É2.&ä·Jê|0‘QГpø¡wE€Ì¦Ž 0l€›‚ñ’ ¼ç•J>ÂÑ ñ¤øéý&û©Ù媩»ÞÈ3–³ü]-…##ÐÈ;‚4Bx|9¹|Ë–óñàÍæaq¢+Í:wdÍ„Bô¡îÔc‰Hl$ûA(þ)”AnH8Úœ<†„rBIA #¯é‰hóÚkJu/ ¦Ù"áÅ®(ÅŸ™ høŒpŒr@,[&|è8%„‰§ÙÙNøŽÀK á7NíØì¶6ˆ iÀ¹æÖºfŸ¡JM£éôHÆÑõkD èS*jÒ‚\`5ÒjÀvp E³29HT ªØ áOúW„ ,6ðÑ6¡%.€ZD°N— PDQ¤õ\ê²"à­qòSeâz@káó”€ÂGSak`3ÎÖy<ŽeÏ© ÚÚrUÒ:&½èÁãF+§u”1KàÙÕ€Ö+Û¤.a?jþ,Ôs(‡ÁN_ »è, €Hà€éÒp@VÅçEð6;T àÄ7 ™5›tè€Ïâ’—|‡€%%°ßRr–„ÀC@ŠeS*|á g•:¾h@à ïT.t9ncœÿèx&‘ŒDRKJRÓ]ƒßò` VÆ(P—îu€ ùh*d:2{·Ðä‰ï»o 0€ x [c%Éoç‘Î0|¬ƒSVƒŸîÌ@¾&8mJƬ‘5g«¸i†ÃFK„=É%Iĸ‘ À˜‘ góÜÍ¿ î’ÏuäËnQ% þXÁY™¬{ž9Mj˜„)ˆF€pD5êR»Z%aÚtm$ý."˜ó ȵ®wÍë^ûú×À¶°‡MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØÎv³?°€EÃ: 'ŸÝ+¬øñÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~ûûß8¼ýûíQ¡>x³×ˆhQm@Û¸Ä}€‰[üâÙ®8Æ7Îñgk¼ã 9±±fgš¸þ®é¿-w¹Š sÇ>]EOÃæÒÀ94fN Ȉév·Q‘}ù£plèGFÒ›Ásx4ýKoFÔ™1ueþ<ÝW¯¹6ª® ®##ëìû4^ ²SÃìÒ»:Ôþê¶ ‚í耻Ûçι›ÃîtÏ»ðN¾ëýïpð»8øÂ«ðà@¼áOÅ{ÃñŒ¼ Ï ÊKþòX°¼64ùÎKóؽçGÏÑ[Ãô¤O½PO Ö«þõ®O{Ñ_O{-Ä>·¯}çsÿ Þë^ò¾gúìOü(ŸÇ/>à“oõá+ÿùJ`~2¤}·Sÿׯ¾«³_ îkŸÓÞFø¿ŸæñÃüä/,ú±þô'²ý½€¿ûÏ&ÿ]Ôþî¹.ô¨ðÿÿ×6€µ@€88 x€0±þ€±à€ Øø *Q­€x¸ ¸$ñ© ‚ $x 'X‚‘‚¥À‚*¸.8 1ø‚1ƒ¡`ƒ4X8ø ;˜ƒу„>øB¸ E8„ q„™ „H˜Lx OØ„…•@…R8V8 Yx…±…‘à…\Ø`øc†úP†€†fxj¸m¸†ôð†‰ ‡ptxwX‡Xç|aLJz8wyXø‡k燅Hs„h}†˜ƒ˜ˆçЈo·ˆŽ~’øˆ•8‰åw‰å‰˜8xšØwŸØ‰ØÄ‰€@Š¢øx¡è‰ˆxŠyfŠ~àŠ¬¸y©°‹¡þ7‹‰‡‹¶(µ¸½¸‹­§‹¨¸ŠÀ8ŠÂØ ¿XŒ¸wŒ•ÇŒÊ(ɈÑøŒÂGŒŒèŒÔøÓ¸wؘØ·hÞ8ÛXå8ŽÆpŽs Žè(~àx ìØŽç÷ާGò‚öŒâx5—üþøºPm€ €){û¸ ØËøé ¹Y‘5˜‘½Ç‘©ƒYyyx!9’Q’i ’(É ,y/Ù’#x’Ë“2‰‚4Ù|y“%a“—“<é„@ùuC”SX”鈔F‰…JI >¹” ð”b •P„MéŽ;Y•+x•Â@•Z© þ^ùaù•PÈ•ó˜•d©cÙk™–Zh–ÀЖnù…pÉ~u9—ó —¶w—xi‡|É zÙ—Œ°–%–bpG`nY0)˜Š°–"€*ánË; €k `#阈 —ŸD™Á$ޛə† —‹š&€¢ù—¤yˆØ°H®¤?Ra`=Å1¬Ùšqw› pšIàÐnQ›O¥›¸¹‰Æé ¼ÙxÕA£yœ`šÄ>²bÉ ´xœàh±©ÕIƒ 0žý…qNä©B×°œLМ։–æ©mÙÀ Àp8fµÀ€Ãùžñ™mZ1á‚þØu!‡ÆPÏ9 ¾¨´ð šúyZ¡áè—ð©¡q¡Æ—¡Z %:¢qp¢N ¢(ê,Zz"ڢɡ2 /º7Z£h£IÀ£:Z>Jtú£û¤«£DZ“HJKš¤D9¤Ãè¤òÙ¤H¥Rê”VêYz¥gI£\z”PŠŒ[ú¥¾`¤J¦:¦¨`¦hzlj›aÚ¦{§Ûð¦r:vZœtz§®étjʧ¶§y ¨+ú§¦0¨„ £{š ˆš¨Ñg¨-©ŽÊ¤‹º¡“z†’*ƒ™z©ZZ©ð¸©œ:“žZ£ªÍXªúhªlªQɪªJ ‚ꪯþ* ±Šª³Jª^z«¹:§º*µÚ«»Ú:«¿ ¬ïP¬Æ*¬²j•¶š¬Ù¬3ê¬}­;·¬ÒZ–Ôú¬×ºȺ­–˜­Î0¬¯Ú­ÞŠœà*’åú­Áš®ª¸®ìš‹çŠ|Öú®d8¯`i¯ô:˜øŠ ⪪䚯Qê§ñ °€¹¯ØJ°Ù9°:‰°ðê® k©Çj°;ÿ:±Ÿª°Ó'±[ŠK ýjª»±©*°"+‹û¤%˨û–'›²®².«­$³$º²’𱡠³4+¯-›”=»³kj³tù³@{¨B 8Ë©:[´(;³L+³K´OK«G[¯R;µ­zµ]еþèµ\Ûµ¼úµJªµqYµb»£f›†i{¶@º¶nè¶l;•p;‡s·bY·xˆ·vËK»·ö§·‚¸~›y‚K±…;¸UзˆËdk—»¸Vë°»‡•;¹O ¸˜+ ñXÐ €ÀPrû¸›û˜—ë$p, pjmkº§Û™©Ûm7 §|ö“²;»¥Y»L —£pPC0 ¼¾{¤½› !Ÿ7`¼f´—ÊŽèòEps»;Ô;©ìxXðI! kÜ‹¼É žèËMѺ­ëunº¾é«§Â°° €v¥;¿¸ ¹ü+ª’û¿Uº¼ˆ þJQƒ¦b`нŽÊŽàì>Rpt¾,Àu'¿M@ÐjEP˜‡‰š&@_Cw¦̹ܰ½¸ àHÜÄe^¦X©ÃÀ‰z‘Åû°™ '_[ÆUpz ˜ÂöÒ,<_Ó)n–\6L¨ì¸°R|* Å;ŒK´ö,M ¨ìÅðÅ/\rIÀžÌ2ŵÅ|*¨©jgì_j|§lœÅê{ÁB,DÜ0*×Ç r,ÆGÀIàgC@nê+žä©¿yì è9žê¹ ê¦ÃILÌáà §¾çleÖÈj‹Ç‚à¢ü\Ê£ÜôiŸøy8 +.þ7Ãb÷Êž¼—´œ š*M#\_J¢K30ÌÄ\ÌÆ|Ì@{³±JðÌÐÍÒ<ÍPËì²ñHÐÍŒ¶ÎLÍâ<ÍÖ|˵¿æ#ðó1LÎãÏÐ\Îçì±×l@>oVÂIœçÎò<Îô\Ï7{Ï E'möÏÏMÐH{ϱ\k9|¼ÝÐä|Ï%ËŽ 2CÓ ÑÑüÐíìˆh&p``ðlÒÎ)m¢÷ ²TS±4¶À$MÓ5}Ó‘û µYÞö“%-Ô(MÔ¨kÓ`7Ô#½Ô4ÝÔþNM»Píðƒ‚cmkÕ&ÕYý»[ý!`hó!¤#ÖMÖeMY>Š%c -Ô'­Ñ"‹"­;ÓzýÌr=בxÖ~ÀÏÈ1(”ÔpÝÐ…mØ‹Ø{_ÛqÐÉwûØÙ’ýŠ÷¼*àÏ=ØóÌ×ËŽáKAmÚÕŒÚÛÌ®ýÚ”ýÙôë§œ-ÏžmÛZÛªðÖ³½Û¼m¡°­mÚÂ=Üw Û®ÜÊmŽÅÍÀÝÜÑͰÌÜÕ°×=ØÎýÜë˜Ý+yÜÜ‘%mš=×Û­×Ý¡³ìÝAŒÛÁ‘oÌôÌÊìÛÖ Þ‚=ÞÔÚ1ßø­þÝú]ÚÔÝßðßîÞ@3àU àr‹àØà›ÞL-ßîà NáWmáÎß ~Û›ÛÍáîÞ>Ö$žàÎàa]âwÛáêÍâ}yâqâþá4Ù6îá+.áF+â½ã1îãùšã-äNäôjäºä®äïÊä#nà0žä8.ãy}ãn}á*nâX.·@.Îë¢UþäWåTæÔ<æ8]æ(Žæé*åANå.þÜr.æNþægî—j>Ô[îæ5çåzçkžç¾çðÝå/^çÊMè~Ô€®ã‚î­Ž.Ílž¹\®å^>éž0Ýš¾èŠnç_¾å}né†.éˆâŒþη™Îã›Î竾þë¶]é{Mç¡Þè£îØ´ž³žëÃmë§ëŸ.êœn•¥~ëÞë’-ì„}êG~ìÒêì´½ìÀÎÛÔ~é…éÑžê{˜ìÃníÅ®ëÒn„â=äâîêÞíÐÞä垬ìNìênì°~í{ÉíîîíÊ îÏ.ïèÞãõ>ïS®ï}ÈïÕþçÌnØñžîÿþê‰.ð²Þê¿î»¾Ù o¿.ñµ~ñ“wîV®ðöþÙ /òOòÏ– oæ&?ïäð.?ð_Ö%é3ŸÕ5ê'ßì)Ïê¯íM°ñ1í=?ñ?ßîð?ô÷~óNóOðsnðþܺòzÞòOïKŸõ*/õxþîÆ õ]ïôD-ö2?òXPÞÐvÞköïõ…nàíÍÌE¿—Gïï!ïØõ½÷Ä|ß4ëöMöñ ÷ž²€ïëŸ÷:Ïô{ø„Kø¦Ž÷,ÍuøwïðŠõdÏ®Ž?ø›ßæŸ?è•O¸—õ™?ö‚?᣿¥o󩟸‰?ù†¿úoõ‡nú²¯ù¯_ä´û­¿ø\ö;/à`ϯ¶ê¸õ®?üª_üeùûºÏü°ùÊN÷Îo žÎøúÛÎýÛÚùµOýážü·Où×_…Çßíäü˯ýÄóÁÿöÞ£âßïßûTýñøÒŸþÿ±¯ü@`‰Eã™T.™Mç•N©Uë›Õn¹]/òŽÉel "8È 'ÁËŸ×ï-•°¬‚oÏO¬,PÏÐÌñ2Rr’²ÒòR쓳SiA@ãã`ãH@䤄ÀnQ±@p–°–,1wp×3Xx˜¸Øø¸Xyù±£tˆaàô/©Î÷—š÷öº0{¬—;˜™¼Üü=RY½=êA€èíÃèmóÈ:\Üûk[Ÿ‘_pÿƹ3xaB…œØ-txà‘2A5€€!Œäû×' —ú Šô÷±¤C•+Y¶t)¤áËvƒþ:|È0 G" †Ü22\J-H¹)•ùjT©—bN%g æ›9•ˆЖ,”F—ž,z¯®±h­¶uû®”ªq‰!ˆ8d¢& â õñŽS,L¯ ¾BØ—aº‹7n9×1§ä‘…Öw 8'›Øò`³$C´tdÕ«YÛÀ6©[›qÖFÈiK¼‚õû™4[“jÏ:Ý”vmåË™?‚Üü( &›2AjDn <`*4ó_ÀŠ« ÎEžŠùYè¡·wÿþÉsø^Шa3»à@„ŽÄ[Ë8â‚+0„›Á oÁ‰!ê@[ D-Âþ,Ô[„= =ü1ù@ì„B /¹)6¤%Å]|*a¬¤Dä2íBO$pÆ}œJÆ×°8 ì0ŠuiQÈ&d&È'µÎD#Q¼Ñ´ cRÊ.½Ä$Ê/"²Bm´ K+”Ä&M1Ý|3’0áT“Ì*Í<’ËøŽÛ²Í9ýüSÃ<e¢F>w,òÎ+]”Ñ1ûl´‰BÑ<´ÌoöœÒL5uBÎM«©SG+yLtTOM=µÓSÃÂóQ¹.=OÐH_]/VUmm2Õ[MÖV“œ•ÃZ—X³_u=öÇ\oå•Vcõ<³WJíD–Ú7•µ•Ù`åX·•Úf«×þÍkUÍÖ[iC%QrÝuÒ\TA5TÔvÓbõÝ|qvSt—ü–ÐnÿU—^} †1^Sýe“`LÙ­ôàˆ=LØÓ…‹m8Úz!–˜c)îw^‡- W[ŒÅíeùÕÔâ=„‚X—WF"f=^Nç¸>f9䌞ö^Esš¹3mÙæ™ñ˜a&êÈŒ†é}–é‹^7ê®›ºÑªºZ‰š­6¹d¯Õ^ lFÅLiINwë‚×¾»­¶}[A´éþ™k¼1n·{>p»ƒ.uðÆ]Ò{P¾oævîëÙñÌU‚PÉ ÷«ò¦Ç\óÒâüOÏÉþ4t­G÷þÙôØ BÝOÕý¶üuÄeß=Úç´ýrØ·—÷â¡ü<òÃÓÎ}ù‘ñ5úãW?Zù¿šyë£ß~ßá>{ܯgœûòB¾óêÅ~ãáž7~0ÑO]}ÑÇ'Þ}ìãßï­­ßu÷kߘZ'³éñ;`ØhÀÛÙ}OKà2¿Ú50i „E3øÀR„ŽBø(A¾/„)÷†Á³Ow œ iè*&J3Ìùvˆ¿þP#táØ<è@6ˆIÄÚ Ó—CÅUƒ/ •XE1‘~N$]X–(ÑŠa¼¢µ(<(¢P†O£ýW®!Âþ ‹fã?¸F6ZðwoìÛµwFýÙñŽX¼`cèGžð€TbÅDÂ4n‘‡Td ù%GÒ‡^Dã$XI/]’‹&Ôd"9II<~O“×&C©ÃRªÐ“]e$3¨/Âñ•‹<åÿ‰DLvÑ–¬Ìec)¥YŽR’Á$å0AXÌ'S™ÉÄ‘0™9AgÂ+•ó“#.ù¸¾j6s—nìe[©FZó›û»æ¾ÆéÍrB™µLgüÖ)$hNs™ñDç<ÍWÏde3ŽY3"ÊOk†³‘-b»IEƒª¡–ThCëøN3>”žýäD ÊÐŽJ£üó§î™%jFSþž!…ÞH{TR:Ýr£J3*È<¶Ó¡ç%J÷ISÞ±tF.-@=ZBWút¥•%GjN}î©?Uª1™úÈ‹êô¨Q•ªMQ‰ÓŠbÕ©<…ªVcT„Uõ—c}éIÉZÖ©>­-¤\}ÙÖ­’‘­&ͧX³j×Ò™õEBMQAjÕ¹n¡­PìbÛØÆ¾Â¯ä*/óºÖ½âSš¦‰Ãf9ÛYÏN6²Ë¬‹«"ÂÎÔ°u*ÐZ×¾¶±­ÞZÇŒvD¥uLU0Þjó´Ü´­A›P¯4µä+<ùÖáÚ³¶ì¬ìPÐê.´°Ï¥jq%zÜ¢&×þ˽êSûªÝ–FºÞÍ.]•K^çê5³æ=+w7ª^Ô¦µ¼–/u§+ßÜ¢÷ŸönS™‹Ù”ê÷Àþý/}—*à˜x¼Í-0|¬àáDºýÕu)Š\ü¾×Â0Iã*ÞúwÂîiˆ'6âó:¸·Xã°L Þœ²˜´.j‰Qa¯x°3ÆñyLUWÈ6þjO<ä ëø¬G¶nM›ä§ØÉ Â𓥌Ýû²7¼LVm–[\d¸v¹Ãß½²) d2¿gË-F3¬dƒùÆovPœ%¤a˜·ÎknòÕªg8C9°<–0›U\è SÙÐÍáóƒüü,HoøÒþ¶r¤÷Œèσn³£«œiN·fÒµÕŒç%/ZÔ§†Nªµ¬hBç—¿—5®e-ZOÿwÕ‚3¬‡}k÷Ú×fÆf°]}g{×ÇFvmh­2f?»½Äζ±±qW D$Òõ" À7B{ô® ¼ö±—ýì)0R,ÖÐýîyß{ßïÞ‹}Ãh_|ÚÛ~¸ÿýò™|ÅßøÑ‡=ò‹æ_¿÷Îoô¥}ê+6÷Ø¿ö[~ñ_¿Œ5ÿù—O~€ý×wÿúãßûô+¿þ¿Ÿþõ¯~þûÞý¬ïÿxoÿø« ðþñO °…/è²L脎 è†àè’nÞÀ³BPGKÐOSPW[Ð_cPgkÐosPwIPçæä6†åŒ@儎匀6€‹ ›Ð Ÿ £P § «Ð ¯ ³P · »Ð ¿ ÃP Ç ËÐ Ïб>`æ€Q ¦ƒ¬c2’.â&®âCN£ßàøÎnøM þmQ‘ÑúPßEï8Kôâ¢"@èÀ ¿"@¾ä3q‡ 7k½Ä€?€î:±>Ñ]þ.Åpb ? ÝD`HáˆÀ+þ$Ž »¤‚?`Z<º¤ >€àâ„ üÍŠqޱZ‚07`çäùnQH`‰ ²F "àMÔ‘…À(cNÀ±|ÑÝQ¼BÉÅç†è*‘1ô…#oò„ð¿d!I…è±!sã!©…ô¸N/åÕ  óqÿ‘"ˆ (ÐM"Ò@` ÝDòÀBNrR²¥ñ´¢î:'zB ÄŽ$ë!#¯W²$… 0Àvb vÒKÀM )9PÇÅþ€î QH`îÂOX2+…`)ÅDGñ€,¿Dpò,MàèfòXì¢ô:R ¨"›ò&+"'UR-›òÀÞ$.!o"S?³ZR.PS&Ã/½„%%ïr ÓîSL.À3/òä à2UEM 4E,Éò(ÙÑM²0ëÑ€Î1»D4ÙrÚ²¼‚7«…/à°òÅ$€>`(‹ÒO:€PÁ8€^a; ŒQLœ:¥³F€= RÒ4¿äè …®:¯óߥ>Ö !ý°1#À ã¢:+éBeÑKF€  @þNÓGÀ³2c?ÑAB#TB'”B+ÔB/C3TC7”C;ÔC?DCTDG”D/aø˜á&Ð. JTF™ÀÀb… [õµf·ŽQ9ÀQ¼1"鎸ƒ"€%37€S=@õYeó]Àaà`Ò/–n—`fWT[kvO±RK Ž®; QãÄÖLÑTMÙ4m›[WVZ6´ö¶n+× *r 0×r7÷ €â8@E]” ΔsK·"(üPt™@sM×u_vcWvg—vk×vowsWww—w{×wxƒWx‡—x‹×xy“Wy——y›×yŸz£Wz§×A‚!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëóóóÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏž;:âÓRÑ4 9ê†)ÎŒ0€A% `h‘`@‰@G™¦ Å) Œ5aDŽ "‰Û2 ,ekÆíß§#(Aƒ”˜ÛÕH"@8@@ß?%DØ5Á4ôYÒ„Ÿ„Fq"pÀ ^½X9<&ðàŸfF¤F qE ;KQžÄƒˆ*a>DÂp@¼…ßc½@"¸~XrÛHÜI´«ñ tûõDxwXBÀêP>HW. 40Õœ6D“  I E  T{ Õ^ p™ñþí$‚€|ÀÔƒE X{NµÀw$< Ö"¡SßœøQ^„èɸS hˆ@ŒCl°U`€ŸNˆàÀT\E‚TAP{Äà€°ÆdŠPÛ” Z)¡Öá¥æÄy€”ó5Ô‘u€ã9WJ\I,`àd€r8A ÀBA`Ý%ž˜¥ ðmªÓ,àWc¢¶d€0"˜¥p€À¢…@)€nDXÇçr{剄®GœÇ+°Ê:*l ¡˜‚ÀÔ *¸jH$¤ÐRú*a% ª(£Žumþpµm¤“VzâpÁ|g­¬Ù@dœ Y'Ádj ÚÄžH` X¸G„ ªµGèdkÑ5 „~œV‹9Èj„šÂ§[:Ñ9D0W2„¿yÜêgõçè A‘Ü|Ç ‘+ IàE/½ÐËÕ±ƒ™,c)÷U20³Ì/ÏùŸ ḵ¦j!I1£ö±Ñ]­ŸZw,D§T{¼¿ A €”ÂÓ6¡ß¯D æKt€Ài@|¤p Á¨r0»°ÚX 3×$&rgø5Þ3j÷!K˜~/ aâ`8!\øðöÒîK„b:«æþìxíêæDäʦæD„¹Nš 1€B4ÞìÅ¿†x‹s ¹ðÄ›`</•hÀG=Ü 5n»W lsûAÒ”À~ŽDp,ÁüDs ‚ |>ä—fÚò¦žÊç]í&蟫(¶ý B@Bë ˜Ÿ m~£ÙŒu»Õ*gøã T¶"pLÒÒM ’ŽE`~ô³j(„†WTA Phh‚­pO!ŠA@g¨%ì # 8p€ö%¡Ò¢šÆsžÅ,r¡3–Öœµð-:?aö>'9á…¬|&àt¼mÎ?íþò¤A˜‰ Âá!MPEÞÑ-xC!¬8!žGWR[l(­ €rÛYd^~ˆäÐYSSÁ˜ ­A¡`üˆÀ™Q 3ŸÙŸ¦ôò!’  …°¥†Œ­Â+u˜ÆYj1x½*!n\W„: /©Ü…@À&S‹ƒŒ¥éÌåä0–¥LÏplˆÊb²CÛñ!%"œBN¥J™0S’­a0Üá¸!:nÕ‚½ ì'§ÃO®6wKàøîWýtÞv–ÉG`†FF¸y&Y¢A8)#Âôx7˜3†s’ÓÃ' ‡°<m‡š!EÞùþ&Éq"„QE4Áœ2JšaKñ¤u|Y†F—Ä’lj‚äéig4U‚±Œ¦R+¤p¼òO™ÐLCÝÚ†Sš©hLjßC±d뀨áßDqóÌ)­(sZ£4Æ( °†“<›Æ–ŠÀ u9yjiäо­ a…¾$E]Jìe´¢uÃ<op€¢„€Õeæç)>pÝd@”‡Òl • [€€ÜE?)Ë7;µ–s¥ ¼tŠm!Ú°Ò-¢«ÌiÅø'6³I§ìŠeQeÆ ²é’,š:#)iÉS°&€m´*uþÜÔÔò5šiíVùFMé±k¶d嘊öð26 x@‚p?$àIª ðwƒÕõ“O‚ÍøMPÒp¬;%!(° °/*)èJÀÄ€\ŒÎŸ„á"¦¸àGÄT‘ˆyÉÐ'.rAR]î²ØÉÞî"ÊeêÉ> %iIÃi¯ ˜ Aøƒ1„§`„D¸HX K˜„Ñ„£…NXR U8…q…Ÿ …Xø\Ø _Ø… †›@†b˜f˜ ix†±†—à†l8pX s‡Q‡“€‡vØz }¸‡úð‡ ˆ€x„؇Xˆôˆ‹ÀˆŠŽ˜‘øˆlç~…g‰”Xk“x›˜‰êЉ…Šž¸x˜HŠÆ7ŠšXŠæ ЍH¬(¯ØŠ§Š‘G‹²þ8N±¹x‹©g‹—狼è€À8‹§Œ÷µ‹~€ŒÆˆ ÊÈ͸ŒÁ7ŒàðŒÐØ|Ò8z×X˜½XŒÚÈ=Ô˜áøÝÇÜ0Žä(~Þˆè˜Ží·Ž¦èŽ.ÕŽu@òh ö8ùxhޝçüƒY{6X»‡¹ƒ ð¸"ØÔ° iY‘'x‘}—‘é !ù‘¸0’n`’$I‚)}+™’’l“.é‚-y}9“(!“j “8‰ƒ5YŽ7Ù“%Á“oñ“Bé…F©ŽG)DyM¹”¦ð”¡—”P©RIWY•VH•凞 É•þò–^IY)e9–œp–` –h©†byoÙ–ÿÀ–^@—rI‡qÉyy—û`—²—|™~¹ƒ˜ƒ˜ÂP˜†‰ˆˆi€¹˜ó ˜X ™)‰9 % ’qIpo³ãI”Y™œx™¢ ²Ò8ñöAÈ9š¢ФI +”šÌ„‰°›ƒ7›£@edo¸É›ºy‰A‰ ¾©qÔ(ÑDµ4Â9œñx ¾i› $ð Í™›Ðù…9N ž¦%ÏÙ®XžŸžN äYœæi•èé †œIÀžDÐÀúÉ|÷ž±ûÉ2$¿éãyŸñéþŸÞà—ÀÈÑÀŒÓ# €à í© O˜ špH³c¹2Ñ¢!Ð$FÀŠ*-º¢õè¡´ð¢0ª2š‚7Z£ ‰9ª£É£îé£wØ£°@£Bz’D:ƒIz¤Í`¤¹·¤Lº NÊS¥E¤ ¥VŠ UÊZº¥øø¥ªÐ¥`:dŠgZ¦k)¦BȦj iš¢nú¦¾§J§m8§Q©§xª vŠ XÚ§@:¨‚*‡|J Z¨U¨Î¨ŠÚŒZ|‘©“ú•z¨—J“Žúº©Ñùwš ª®©ŸJªçyªÌ8ª¨Úƒ¬ –Úªhþúª[H«²º§ªš¹z«ÝH¨¼Z¦ú«ö¬Âºˆ¶ †ÇZ¬µº«;ª¬ò@¬Î*vÉZ†Ó­š­Ö ©Õê–Ìš­é«ÞŠ~ÛŠ ±ª¬Ø®Ä ®èÊŽãú†íº®p®ðZ‹Ýú£óÊ®õú­÷ª•˜¯û —þ åZ¬òú¯Óø®x°Û ” ›ªêú°YÚ°Mа+›«”{°+¥»±°ø±’0°ÂZ° »ªÛ•'Û«Òš²+«’.Ë¥"û²Î8³ñj³4+Ž8ë$û«&›³ú*ª1 ´J:´aj´DË ?›´Û²Lk Kû´‹´Äг¼µR–T˰þYk¯BÛµAÛ¯`Ë’[›˜;;¶1y¶Œ`µ·Šµh˵Nû¶[¶ŽI·r›–jÛˆy{·Si· ¸·|k¦€‹l+«n¸é·u:¸ˆ{zŒk…Ûª‡Û¸~ú¸K¹±˜ §–K‘‹ª“»¹¶º¢;£»›Š[º‡™º¼ð¹¤JºªË©Á. °}Áme–§»R°$ -~s?kX¹»¼ ûè"÷Óq0oÅ˺Çk™Ð›°9çp¥Ñ»¸Ó‹gVT×Ùë”Æ»½TZ¾N /rt?༂۽拺ÀpÐ+ Áö¾ñ[èÛ%€7¦iÚÛþ¿ø¿N°° Пaຠ »|„\³ð;ÁÉXÁJa"\À ³Àð@ ™Â!Œ£¼$ &L™›I&`uv§+ÌÂǰ)Ôtl$®Á›*“ØûÇ™ptPršÃ¦ÛŠP ÐÃþU Cðh4S]F|©û¸ Æ p~ãÀÅÆ™H0ö/_<©û8ÆbœŸPtJ žFÐT±¦éôÆúZ¬KÅ¡êãÇŠ ÈÔÉÆD j€*Å:Ì =Éà‘LÉJô9MóVo'“\ŠÆŽÜ ºŸº ý¦ÄlÈÑ_OŒÃ¡þ\ªl0Ë“<É´lÉLР¡ŒƒL·f@t®üÊJË^¢jb“J6 #rJÅÄ,ÍÔ Íƒ@@\–UÂ*LÍmjÍ0S¡œQjl̼Ð>VÙÙœüëÍ® ΀PµqtÐgç,rÞfgò ¹èlãAl 4¾fÐ GÏÿŒ¼!µQ@£Û3•P}ÑÑ]ÐÐO«Îp``EIÒ*½Ò,ÝÒPÍ´ûµhÓ"ikŠÒ.½Ó- Ó ½ÐÛÓú´îV¼:ÍÓHýÒBM´ûE}ÐpÔI½Ó> Ôþ« 0PORMÕUÝÓK ´!–Ö(»ç Öa½ÒWÕŒùÓPCÇ'ÝÖV=Ö9‹’"~s×x-Ör ×7¦×îÌ1‰2j9Ø‚MØk»Ôð[OíÏ]ÐlØoíØÒ ðL¾—×™­Ù£9Ø{`¿o`ÙŒÍÒ£MÚ—+­¡ÝÖ­íÚò Û«íÖzM³™Ûa=Û´»}Û*íÛ¿í¹¤¼]ÕÄ]܉wÜm Ú­ԦýÛÁÝËÍÜ{PÝÂ}ÝØ­³Ó=¦SÝÒ-pn”MØÚ}Ûܽ| ÷Ý ›Þ«½ÞV`Ù]ßÝÑî½±ðÍØò½¨PâÝþßèíÜi›ÜI-à³÷ßNàйߘݒýàÖÍàÃéࢠá >áù}±.Û.áÛMáºÙá½ýá ¾áKâÊmâÞÝlÛ-~Î ®Þ"›*~à,â(þ°7ŽÔÞ»®ã.ÞÉ;ލáãk:ãñ]ã¢Ùã<ýã.ä4^äëäy°~âCÅ0.ä2®å[nå. å ­äüÍä•)æýåHîâjÎÚ9>å[.©].çlîåCþæ¸åR¾äTþ¯z>Üqîçsè)Mæ˜Úçgþçûjèã}çvžçh>•îヾè…>éF æIÎén®é‚[éO~éÎè÷êèˆÔfþ^ê™nêµ*êW®»«~ᮯ¨Nê´Þê< ëcŽë^ëëzë|>ë¿®ëuNèŽì’ìxËëkÞémÞÝÂ.ëž.í ®»ÎçÃ^íØ=íÉŽéa~íkÍíIì%ÎìáêíЎ矎î×zäì^—ŠÎêáîîn™í{NíÑÞíâ¾Øû^îä^Üêî¸æ¾âön­/ïã­ _Ùóžëõ¾ë¯YþïßíOÝ/ïè¾~îÆ.ª#ÿèëéíNñßz_ììà¾òÊÞò4Oï6_ó8òó oé ï¬ð/ïÚEO˜1oò3ïóGÏÞA?êCo®þ!ñ)ŸêOõ±Þô•xõ%oð'ßõ??™Kö\¯­^ïññÎïSO нöF÷ßöyï,¯ðcåýmç½°IoñeÏðº›Ð]õ™öú.÷€OöÝøß…O÷#k÷7÷O?ß/ô‘ïòŠôyùZßë’“ïù—ïß¡ÿìLmøJøß®óÿù)Îú‹/ûTpñOö©¯í›Ÿó¿ó°où¹¯ß´ó®ü¿û§ÏãÅOöǯüÃú¶ï÷ÍÏÞÏ/üwñ»Ÿï2]ý˜ýÚ?ý@¾ýßûNý¨/þQNþ$?ú3Yúº¯þe.ÿ§îýþ þqŸýù_ù{mþÿ·Où@`‰Ea2-™Mçú*FëÕ‚TF¹])j½eóV¯Ùm÷—ÏéuûOŸnó}ÿÐiŠBLŒ¬oªªë0/q‘1)p’²Òò3Ss“ó‹¯4T€ÒªïÑÔõNu•¨Ut–¶Öö7wôS·—®B À€#j9™D¶H¶îÕ9Klú¨Úw›»Ûû|m/œÜlA@ãã`J@䤄¹zT1_;õº¯r 4x0š?„ä:°cÂ`€;…÷°e˜ß4}s¤9Û¸dH‘#»#ùí€&Ç><9v‘É ŠÔ`r¨q"Æþ™Oöôù¨µšAg8Ð$€ OÞ  Aˆ/øpÓO8XWi%úlX±&LŽ¥U€A“0<ñ ¡Ã‡ Hm"³¢W7\MémÃ’_³ƒ WÖ0'i™¬mÛED€óæÜ·Ó²ÍŒ3'öü4-Ä¡-!8Ê$©†2 TÞmVÙêÖ›œc¿¼H0iÝ»y۬ݛ΃+;/aÍŒ@å ¨â..{sÖç¶gKÿ {víL60ðÎÁôío[² ¢—È“]7·îçv¡Üjâ?~Ý£õÓ9§ÁáÚ1aÎÀ0 Ë®˜^ ¾êººú˜¯¿þ 5üŠ¿ ßFb˜pMˆÀ€À¦Âìº %ì‹Bqf ¬FuÜ1¤yÌ/­‰nÂm$’F#\’ÉÃrlÒŽ «ŠI•LÃÂH®„’Ë.E{ÒË«œrH©43Ì4ÕÔÅÇ5ÿq¯È3a“SH7í¼s“6ñ´ Î$é$Ó‘ÛrÏB ÅÌC½’¶?-sNE%T6B)}1Ò@«ÔÑ÷.ýÔE u F=ÕMT3%•ÕPõlµ Sã„´Î6•oTXueòÕ]1­ÕAqµ´Œ,¯ÀÐ×dyìUÙRÇ|TU`£Ö>b›½?f›•ÕOZµ5UlÅåRþ[e¹µ²ÓY£ývÜv,7Ùs9õZpWu_áõUÞaÓívÝzó¸¿}wí·ÚÑ¥÷T‚Î6WWŸm8Ø[fX݇5ÎÎ`]¾0âX©ÙZ‘-&yã”±ëÖµTxÞ€)V™æÏXnÕåcCæÂØSvŽ¢gV~®™h„nf5gŸKæyä—1¸è¨;šÔ¤…^è¦uÆŠ ŸZê°äzR«¿&›™“–9c±ÝŠj‰û\˜m¨+÷í¼OŠT³c;m¼§U{k½ ßpEý#q WfŸ$¾?]Ü¢È/®›nÊ='ÈòK1§Is”ŸîüóÔÇÖxtdÏðþúo´]{üjÕm_ýáÖw–ö³K_ûöà{ RÝew\pîžùZˆ/{â¶Oþ²å›¿”ç%5þ÷ÂOûð;Ñ^ñèížÞú꥟}KÈ?”ûï%çüöí§ä}Cã§~{׿€:žþ̇:ôýO3Ô àwÁºÖï€ìŠ ÀXAß8pnTå;Æ Ð‚Ì_¡öç? nð„!T¡@¸§Þ-} Œá iX¬Ý‘ðýƒ!ÅÔ»ØÕˆô¹¡ s¸9R…H â=AvPk“Ÿ™XEf´O/œàÌ’ÈE+Vq„DÌ ·(½.šñ‹L c‹h:þQþ±ŒçK£‡ÈÆ1ÂñŒrT_ çÄ5ÞI‹y4  5ØG?ÖmÞùE~Ð_ü£¹G%ʇ‘´à$ÝTIL6²‡OÔä&IIEzïn<¢GBN®É“Ž%tfÙʾRM± å w™I[а”<¥S¹È8ò—ÄeštIK^:Ó—Élß2ÃÔLê<2sST¥4I‰ÅDÞq›Ç,ä'/ÉÍûQÓKÖÜKk§Mcšó–Á„å0ÛYLT®ð´:»¤Î¿°Ówî¼§>ãéMS‚ó–d¥,ŸIÐëñ“\ô ¨=‰‰O=:T|…’?e„MÒ ´¢ݧ¡˜ ÛÞ·¹míj=£Ù )öuž}*Wù* T¹ÉUîr™[ ¬•·„ñ­†€kƺõ²P•mt7 Ý~r–°þ[ÍëvMî&Ö»}­xcÎö&t¯´=oSw{ÕõÊu¼îíEË Êù6½Ý½oh‰+_hú÷š•ýo†¦Ë`ðò”Àù…o»ºàÍÖ—­þ¬võ_þöÁv0†Óù`Ðn¸À>p…EðqtQH’ Àñ6¸šð_ÙÛ  ÁIòu8ä %0À6à ã” ù’wð^{œá"·» Ó™=PÈ@5TŽ€_…¸Æô¾svnsd¥é”Šþúöná4h‰`€&` D2GzŒ•~äU1Æß Ú/Åvèñ¼00:jàñ¨4¡!1wÐÆ¥m~oØí _wO/©ÂïêcA‹ZØò„@  €yönïÀ•Ôµ:<ÈÇ>’ÍêóD„;a£–<† %x|ä'/ö'}æõ>{ßÏý•Ðïéö¬•”QRw' €=KÈ»$¯ ùÉWþò)0v”Öþô©_}ëOߥ=Ƙß}æ;пþøÉŸ}ÒnßûéG>øK‹ò¿¿úæ§úÕŸ~ö“6úð׿üé‘ý¿¿LËÿþoüø€ßÏþ0«/ů¯o#P­ÏÝï©o7ûL«<ð!P;°µOë$@µL ë¶® Àë˜ìL„;r sPw{ЃP‡‹Ð “P — ›Ð Ÿ £P §§Oʃ ‚î †nëŠî H`æµÆ ËÐ Ï ÓP × ÛÐ ßãPçëÐïóP÷ûÐQë6`$e@Öa@„ÃDTŽå\.`Pç"Qe".€òHÄ+N0N;Ñ?CQÃ'îP$ï¶Fï+J Î$@ Q2\þDMXÑaqDnkÓÄ E€>d±hQ\LÅØB ? ÝD`Öáš 20ŽVnÃ$§ª‘ „CHkM2à8à`n Bàâ< `› ÏcžO–‘ò…¾± @Tk ì¤Áq `8ðdK@2 — 2 [®Ž ²NÅBòöñP2õV/ wQMA` 4õ@à01óˆJg0Õ C» JàJ™ 3£”K9D’“ €Q?‡¡é>VDÞÁ9 M K2 ÑèÀ>`€9“.€B; )aR0C-àñ¶à²K•Dà0B@6™ ÀZ®¶³3ý³ "ìJì ;Ó”²Ä2ã@ < 1Wþ2)`DÖb@dÓ.@AàIÉNé¡AUXý@—@L—€.QV-·Ñ* d-гX$!Qî<@H—@8\$)EÒG•3WPK{uXË•X©sR— æn €1îŽ Ó@Þ¡RÒ²ZŸ1MY¯’²8Mà[M@6ñ‘WÍaóàÀ<ŽÕ `î€\M`è D69ÔÒ ìÔu’O?VP`Ù”6+6aS6ŠU`yYÙ›@6çuòNK[M²]uvÛÁ1>öVI– LÖVeXÖag6bµY™àYöôÌ’c› <²•@ÜÂîg™ PÁuÁDàiÇv Ö2]fçΉ¡SÂDÀ„"Góó…Ôô؆JÐd a™&-Ȳ²SàR3ÂRC¨mÍ=pÀÚVð) 8m2 ÉªÜ '‰Åõ,B[&\aßVÃHP‰Î#•-ùõ›7†¯€€ xÀˆد°¨|ÕÑDD†õæV´ù Ì¥–Óà¯õTP€} Y@”¬9#u6Z8ç ‡Œ¶Œ‡Öð€M’”D`ìÑÖ=@Îpá˜öþs$*$*‘L)ýníYVHÝ[MêVÃD:)-Bt³V»úÖ*‘Ψe¤é]ø†MìbûØÈN¶²—Íìf;ûÙÐŽ¶´§Míj[ûÚØÎ¶¶·Íín{ûÛà·¸Ç}ílÀ_ÒP-ãŒ`h"œÌ;+c¢¼çMïzÛûÞøÎ·¾÷Íï~ûû߸ÀNð‚üàO¸ÂÎð†×ÁÑP-,e5„<ÛW‰ÀÍÛÈÍñŽ{Ùø¸ÈG>î“üä(϶ÉSÎò–;{eÙƒÆÉ ì»wïó:p£˜› c§®FN…®¡OèÔhÛê”gü–x¿Hdžѧ1uiþTQGÖ­quhtý_oÆÖÛ1vj„½ggFÚ•Qöu´]¯FÜ©1wi¼=wǵÞ‘÷sô}ï€ßÃßË1øÀ¾…GâÏø7,>o¼äÓùoT~ò˜Ãå»±ùÌ{¾ ßFè?Oú+Œ>§/½ê£úk´~õ°_Âë«1ûØÛ^ÉzŽGíoÏûÝÛ=÷¼þ| â _õÆFòÿùå‹øÌ¾œÏ êK¿ñÖg;ô¯Ï}ÙoŸìßï¾øqtvdüz??2Ô~W³ßïo?¥ãO úË_ËöFþïoÒýÃÿü§Cè €îR€¼€€ø ¨þ ¸€8ñ€¸ Xh X2‘´Àø( !ø,1‚°`‚$˜(è +˜‚&Ñ‚¬ƒ.82¨ 58ƒ qƒ¨ ƒ8Ø»«3´õš³ð:²D›=›´ßê%Û«KË´Ú ´RÛ´ÂZµ5«uH‹µþ<ºµhèµ\ Q¶:Kµd›±C{¶zj¶j{´F—m[´i·¶mh·t;•x‹˜o›·|;·~k¢}«˜ƒ¸|¸·‘ð´¸:¶†ë°ˆ{‡Û¸pZ¸k+¹½À¸–›©”»ÔÒ °¡l¹™«I•!¹çªf¦¤[º‚úº[0"ì“{h®»¹°{²«`9¹çÚdŠ;«f¯É>0¼]Ù»»;Y.jÄ>€»g©»ÏëŠØ{‹Õ% »6ºÛ›½~@%Ð6ª«ºk—Å몹° Ÿkê¼æû˜›¿wZ¾ˆ¿ü‹¦l}Ñþð‘ v¿þÀx@ Y–%•ÅÌÀ´ºÀ‰@`kEP•©ºà)N§_„ZÁ\œ`½µ!:‚Â:ç7’/﫪1)¼OœI`q$dgœ)|Ârð’%Ð,|_Ù©L‰ön‰*Äžú € 0Å£ö[:Œ™F›ÖÄNŒ“A RŒd¼àsJŸG@TªVk^üÅrÆ— ÆG0k÷ÆpÜ¿æ™ÄºÓkíRÃ¥Š67Èà6gÈh|_€žµÄ‹Æ¡íùžWœÇ—@ŸïyŸ»`o9ÌÇtq>ü ”쨽à¤\ȦLʆlNj5j8¿þÔÃd&ÃÊ¡œ«Ö0¡[Av3B#l"(\Ë©°¿ÀL°&L$ðÊ,Y,ÇÃܼÎÜ#0¢QÔÌÏlË¿àâ£g% Åy½Ù\ Y¯|pgä;Σ ³ìk8¬·ÑÌÎ…YÌR0¯Q%£¼#Ïôl«ó¼ìe€†Íÿl¬ÿ÷G¥ à:õ¼qØöf -xö<…nÍ;p}ÑI@ÀÑz;pÒ(Ò*½Ò@}¶™ÇåíD¼@Ó:½Ó<ÝÓP/M¶ö' º÷‹Ó>½Ô= Ô½Ð*Y<'¬ÔL}Õþ?ÔaË’"ª[ÕXÕN}цàßÜ0{Âi`1VÖ>=ÖdÍ»O@[ z7íÖL ×q-!-ÎzÛÖ|½Ó~ý×€@Þ{‘†}Ø9ØŠ}¾ZMÙ Ù’=ÙÇXÙ}ÀÖÝÔœ]µézÙ‡Ùš¡Ñ¤Íצ}Ú œÚ¨½ÚnÝÚ®}£°­´{ýÙˆ}ÛD;Úº½ÛsMϾýÛ‘ÍÛA;ÜÄMÛY`VÚ–×ٌܿ­Ü±ëÑ® ݺ-Ý!J,½Ý,íÒÁ ±ÖýÙØM£PÄ]ÜßݰáÙ™1æ}Þã=Îë]Úí]ÞçÕé°óÍÚõýÞÉmÜ1ºþß³Ýß÷ßÏ àù(ÛamàÐkßðà(*à Nàžß+ábMáÿmáþŠáWÍà>êßÑ á#êá}­á#ÎáújâK âH*â×MâÊâoâ1®â÷Jã ­«î]à2¡:ÎÓ.^¥¾áÕýãb›ÛF~¿0.ÞH.ŸAÜ£Ûäìãòå:=äú[ä)~äVþ ž]áSîã_¾®XŽÞc.æ§}æøæK®Ùl®åcÊå7îåLªàÎãtîäe®qnã|nç‰çèU.èZKè'®çTNß}έÎèdŽèN«ä]Î䓾æO¾’–^瘮æp¾émæo^ÏÎßž­‘þîæ—ê©þ–ŠÞâ†îè”~±^ã’ꓽêŸ^êŠÍëk½ç‡®é¯N²è½Þê».ê0yìÃì§>àÅ^®Ì¾¤®ìpí>íèZíjpíží™îêw>î¦nî¿îí?yë;Îêá¾ìÜþµì.䳎êµî¬ó.åÉþîéïƒéì´¾ïÈNîz­íy~ï˜ïY^ïÒ®ð˜Èðh.ðÏïå®ëÙnðq ìçŽñÍñïñ¯î`-òÃ'ìOð‰®ñ'ð…îïPKò…Íò¦‹òöNìïëOóP-ó6-ñmNñ)oñoò5ïò‹ñšôrÞ=nô=óø ðþ7/îPÿÏ ¿óW/Ü>ß¼LïðÛ®ô¾Iõ/ôUgÛæÜÞõ®ûõ¹®ó\1Ô}³Y÷6_öÁ®Ýܽ÷'íÝ;[÷-Ïó±‹ô².õ7 øG/ø!Jø¸þ÷l–nïîßñp²ˆï¾wöf÷1{ùƒ¯øäÍøíN÷?º‘¿ùšoõ•ﱞ¿ø ¨™Ÿð¤oø‰Kö©OùØ®õ«¿±­ú¯Ÿ£±ÿòŽOû{úªŸûv/úôNü(Ùû°¯üú~üü¾²ÎüÐßðoü–_úIýû ~ý/ý“ï²ÕÿýÞâç¿âÜŸ÷éOäàôâ_ñä¿þõlü¸?ýÉßþWNÿ²þDÂÁ@° ‰$ÓñP,MçµT ä›ÕnMÊŠ,¥Z¹e3ÒV;Çg÷—ÏéuûŸ×ïù}ÿ00K‰LÐð±lA@ãã`CK@ä¤K‰iM­M0Í3 4PtT¬*q•µÕõ6Vv–vPµ7פ‰a@òv‹¬ð´øéø/Y¹‰YWzšºÚú›0›ûïAàJè#K¨P‹øyYà+m½¹ýÞ¼û?_Ÿ?n» ™®„!ˤ4a«½h}œ=›¨m½)ñvôødHŠEò+ÀàʲxÐáC†^A'‘$Fþw8íù©¨ìbI¡C‰Õ÷Ϩ>(‘¨dYFD€›–hÜØsäÎwAõü,Æ5iX±cɆÊYv‚H j8ÜͪÁâñŠªî»£ò¢õûp`¤§=¸hHð8AD¼‰óìõÔ·Žå5˜ wöüyߣ   º/H3uª\Éé8ÏÑüé´ÞŒâs 0ÀˆVkËÑn <Û¼£-Àó <A.„K0››¾3oÿR°«¯£Áþ 5LpÁ ©q@ í²¨ý!ñ+=\‘EÀ:l/4DÂ(ú;Q«aÜ‘GÔ^ì1KQ·g4ËÈ!\’I²~l’!S$2;'£²»$§„’Ë.CzÒKçâ IþT|£F(n “Í6ƒ<ÓM:¤¼RL-é,“.8ãÜ“OŸôìÓ9ùú³Œ4ÕÁRN+EÐF‘ÑGáô2BT´ÒHq4SSI=ýtKAMbÌëäôÔ<;•UOÁlÍRuÄ“LS0ÝLTXu óÕ] •õH[í\4ÕZ}=¶Õ^‘ÝOÕbM¥õÙe¥u5WP)ÅuÕ@oÅ-Û3 ­gÚpUVþÜk¹uvVaQ—]>É ×Üíº5ã[hª¥jØLÛÝ×Íw§ÀyÍ[tƒåa(ý•à VÚtžÈ…—mØÆ{å"øÜˆ¦ä-Fc55fŒcyÞ2ä–YùØ’]ùNu›ug aöUfp=†Øfcsú¼wíÙ^/M9`š‰%jòŒÖ髜Ö÷g–£Þ¸©a­zÍX™v8ëš¹>4¯“hvÆÎXéPß69n´íHmVÁ>™Ô¹g.ûé»ÿ+ïQ÷®Û¿}:ÚÁ«pkÙÖÚíu—øñÌ‹ŠüÓï.p¬5}(Ωµ®myOúóŽIþýK¾õÓ¾}úÇY¸GÞ{ÑÁÇÜ~ëËßW±?ôõÏvÿÃûîG@"€òC ë< ðôâ:7A•ÕŽ‚ê ŸI8ž®~¡ XBB*gócáu’ÂÞP@'ì›Â2z8â‰t(=Ô5/€+¬a‡ØDzÅÏ]<$¿'Â:‹¶(b÷ŽÄZp‰Ìâ#Å.V1‰WŒÐ¶~8F7Êþe‹ü;£ÿ¬HÃ5 /lo|`÷$Ã0*ñŽ6Ô£ù'?’‰Tã ‡XÈ~In­kcÁÈÈFB±¤›${HE:Z’˜4¤&ÿæÉR‰ ¼d8ÇS¦ÑŽ£(Ué8G¶é²¤.YËÞ’M¹¬ ;YÁTúò—´Ä%)gL/*2–ÈÔ 0yÅLÖù°˜¨|¦4IHM/ sD³Üb½†ÇMz³Kà,Ò.‡)Ns \Rg•܉Í)¾Ó…ñT˜5ËéL4~ñ˜øt >›4Ï,±3œèûÊ$ƒ&ªž¦ áB÷¨Ì`ò3Þ"æ=µùOа¡Kz¨€"êÏO~þ¤­¦+' ÐmòRŒ(-_H+†QÙ‘3£O,©LSÊJ ²t’°¬$LÉSòÑ´G#õÏF#iϦµ§1´é8™ºI§ZªùC*”º©^u‘YuÞVwÔÕIUµ”&}¥XµGV™Ul ¥§\Ùú:·Šlªœä¨PZWâÝõey½jZ;zR>”‰UìbËXMøUz>5#]!JY’Zv©DÐìf9ÛYÉBv<€]\µµS¾¾t˜¨ÀjYÛZ×¾¶P)h{#Z‘V£¦u©GSÚ¦s¶Àj6OË[°F3¡_ý-p?;Êáîu·†ímQÛ‰Ùå–5¸ò¬DƒݵBs¨þ×Ýgsùܧªµ¥àíku•+^‡fwŸæÅ*z»«^Ô&×·î­)y—)_ÂWºÇ /~©«ßýJÕ¿Í,ìw§V;ØÀ\…oA·K_âz7½ Fn„±Ëß‹&øšÜ½°}ËÞür¯^©uϪ[ØÄFqŠÌ⸶w8ž«Žg|Û ¿Äý\p†¼Þo¸Ç>Vñ7+<äújxÀ9>q’uöc‘6À .ò}¥,c*WyɵqiÇœÛ2ëôÌ_Žm7„[4ó¸²p¾¬œÕl 6W9È9Xš—Æg¹ù¹Î>²rMó|S´*8ËDŽ1„ ;gÈÍ{¦³W§çJ7ZjƒþN*–1üä-—øÈQÆ´5-áBSÕÅP6r—=êL‡Y»§Öëyp‰‰üßä ÌGEóB»7†¹0,,ÀWÀ@|*nmœº¤Lɸ§É¢tV9]r8GþÍRÎJd¡!WàÅ«ctl 9Ñm„:»E”±êì;”:hN’’•d!¸Âàž®¿ÛÌx³±Ò¾ã• ¥ï <ÿÖþ™¥¤$OAB â>÷º]ï’þ»²7ø9O^$–”æ£XxÏ ¤ WÇÂbs„­cî  ÀêYßz׿ž€„b=°Ûß÷¹×ýí= X!LöÁ‡½ì7@ûÝù½Oìï…ß|Ö_±@þôs¯|L0ßù͇~bkO}ï[Ý÷þô;°XñÿøàÁÐ?}õŸ¿ý¹/¿ñã¿û÷×ßþæÇ¿îÕ/ýýãþþþ÷«péÏ0þ}¯ç>à±L€ç|î  è`èä „ ³:Ð?CPGKÐOSPW[Ð_cPgkÐoAÐæE5XÃ5°Àä|å²€6à “P — ›Ð Ÿ £P § «Ð ¯ ³P · »Ð ¿ ÃP ǰ±>`ŒàS Œã’Ã0ä£á.â€;·Þàì>4ßÖcßîP‘ Ñå íÍSèn³<-J $`ÔP*ôÃM$‘- àC³.°M<ÀêcÀ@@ù%áÚÐS<`%Žþð"pOD@à.à ¢bß8ÀáÐMpQ€‘À0,¨ƒM2à8à&îB@ß<@Ù¥›clPdÑî<…Œñ @k ø¤ñ=cñâdK ß# *Úq_t xƒîÆQRÞqñ5DÑM òåÑS€ÎOî’_@ÏêÜâQdq=`êPÒDà ®  pO@Òà>` `÷„ñ¦B$ J§åðšâíÆ%`B ¸î#Íñrñ2ð$‰ò2<À%Øã&Û¤À Œ‰®]ÚN'ïSþH`Ö¢Q@Ò*)éQ)³€çhrO$`$M`,M`è^rZÔ"ô0rT ¢r!•r&IÒ$͵ €OØRñ B-çã/ÙÅ`[Å0ô²M@R"¯"“20± &÷äÎÒ²9 @2¥MàZÅ+Ár(Õ‘ÝÑ3w®-Û¤3=ñó1$À6Û.Øðñ$Å$€>à'ƒ²Q:€&Á8€4Á; ‚1Ns€9K`žÑ: $EÓM†ŽhÑçž3:µaÒc=QR0`¢- €³ä#V±ÛD>5+F€  @F“I þ³ÐÀ>QA”AÔAB#TB'”B+ÔB/C3TC7”C;ÔC?D”Dr³à: D[ôÊ1îC&A7 J9²T²>õÍÀ>`À6E.à?;@$1’-C`*àÐp³ HçtD`0B 3‘ À ®ŒÓ¬±=:à°òà 3JkB%  )ùm </'R$` CT"9:Ó. :5 ó”NSÄñ”ôfþ`"§R’9D’#?ÞcáŽÀ4Xô C?DR!Q´UwÒ HUU›ÕXÕöô  ê|•)² Ø’9& 4yÕ£Ôñ"W{qáØ²' ™ÕYÛ> ÜÃUM@+}UNMr LŽ9:ó@SÒ!CÓ\‚LÍUM@©ôØÕ] UZM௪nb‘ 3·µî+X¶KV#Á)ÌõS ö3õÕaW¶ u^/Ö^€V‘ÀVs¢R%±àW‘‘èF ÒôX…Qä[YÖhóÀ*#¶^»´ê• u!äcè :8 ޱ1²›C(ÕRS‘Õ<µU a± ü˜ñh×< ^•OîÕ-£T"`è"°0‘ô(.k»ôKÃtLÁÖX£ÕdeC J€nÙÖqWá2ç r—rõ Ž5EÏÀK+×só!fB7÷ &÷sOuSWuW—u[×u_vcWvg—vk×vowsWww—w{×wxƒWx‡—x‹×xy“Wy——y5'!ùd, X… 1"""+++444<<<MiCCCLLLSSS]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««µµµ¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›³Dx0D'þO>{p %(%£i‚ÔMSœ=0`ƒJ8  @ƒF´rõ öR£gwU»Ä® DÑ€#ˆ@¢“¨¥LÙžyújÅ @ðà@„$x ¡AˆDž\ùrf?%Dú3ôèÀ?¡„¦«q"€»x°6R¸ ` ‚ÍŒ<µáŠv– ²üˆN§M|0@è€è¨êmb¸ñ=‡ ÂárÙ$¸ÐÍ—¸™ßÜÃx/ÿxÃ4g@ƒ"0D ( jJ€ÐU@°›P$PÙIPÄþçWq¨‘"à>‰°`•Ý^XœŠT-0 PuÀ‰IèÀxE$·”émÈžs;…°¢‰Fl€@€`°Ÿw4pVED8a ð÷ÁŠ<°›O#¬x¥0a…J`×›˜Íh&–ö5Ôu€çÁ%tCd÷Ÿ\^7ÄtƒQÂÑ ¡AI$€4@ÝnPAð› /QÀiý”Ö,ÀØe¶ö¤c€`Ôt1ªP€@¥’…ÐiÐö`n&Ê\^}AÛë +Ô °*­ XƒÀÍé„*8pBdŠm§þ]§A€”2pi`¨:&.äšÙ¹[ ùA¦\pã ÷m¸ãÖÙ›Hø&£I|Pg–1Ð(Sq£Dxp@£pÛ^Øù'Ät :Û #é’&Ä|á9 dÝÚUëËl…Ê2Ê:€ÁáÁÈŒh€D°?£§²ÿB-ÁeÎÆü˜  @sT»M÷¼² w8rb§•€ô0›BW!›-v ÛšXsj'6DÛ1Ý3”)|PB ÔD \fWŸ Á¸oB®DœÆÇHdJ§–.YäÒé}öØB´"p>·Ÿ½I¦þŠlÑ**ϦãÀëÑ&¡m½D 6´ÓpÂ@ÕÀmMD°JÃÞ¼!˜>Ê© 0ÄèD¼º ˆ}n‚eËéD½ X/övó<)}¹WøB‘/9²~"‡»{µ yß_úË J€¹×iH`ÔÌf/DÈΆp+áÌT‚ñVŒ6³¹,e¨#îŒU+mh|Døó“E5E²Õ‘x£8GíF ŸÈˆð¤!ÌæA%¼Rf©ý‹À1– z(„HF„Ùá˜ø>„ Eˆ ¤š$Eþå‚É!á³b€.a=Å{XÔd@ l:!TþÓò! ±¥/ìû O¸7âÔ0u(BâžW¦ ¡xe‰áƈ¤:’-ë1 Òöò¤Qm$•`Ô”F»€¥’wB$‹àÆšR–¬"B’ãAkùì™Úâ,ÕªX–—3ç’0‡’ËíˆÐ• ~ðT¼Z’!áî;4AP,$ÐÏix i8"$çjÀ¼¤™Jê…9UÙ^/{DœÄùðœ|f‹@EUd82Bâª<Ü •Àbþ¹Ä€&Ac$Ø|IÏA&NéBôê(¾í¥m®[D— ½àTt{‡ìæ"˜AHKP¤(gþÈ>å !XŸ‰ !?Ê7¸ó|=ÛãÐö|§œèÓi J{"ÄR²¼“KE¤G`7w ÀiîôÔ!DcÚ˜4°Ðue›ü+K×JÆ¿“INk¬)»„˜Á;¶Õh&ÈÚî@jAüŠî’g±¸3%¸©M5Ay,e…ÒH`%ÂpàÚ—NÇPü³T؈CE’¥Í¬tÛW™S–¢êok &_OiÓÕ #ÈËxU›EÎÐR“Š€» ËïѦ]´iš>p€e •'½ l 0 Á\›*@¾ˆžA@¹rDK[¿,V‘)Í…À «×!DóS+¤ ldCþ~"A¥Òd)ír%­yñÓMØ”¶ÌØÊš¹œò[Õ%Ü7.t½)N±eOQið×x<{Z-x`»Ui@‘@P…IÔ à"aî¦x’Ö¼d@Ä 1/µ'%„Àp1ꥥqi|PÓ"P‚ –Ê¢IšJ´êÅÑ!ä¸*<žæS¾KR"€Àj €\Wôæy>QÒ…½'XGi? †„l,Gè½Ek2æÑS, Œ%<Цb—̬ٴ ¾Bpà¡ 0€ H…°×`¨!ö^ÔL‰yVC£¸$R"d6ASÒhþLÚ]æôÈ×Èꉅ>8€1y®¨Ý@‚@š|ƺ“ E2MŽH…+VÒí¬á ®LŸuxg õ°—ý7Õ–˜Æ™Ž²™M픸IØÊ5½ŠÐ€àÛà·¸ÇMîr›ûÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~ûûßóþÀ° ð„°¦&Ô[´N:üá¸Ä'NñŠ[üâϸÆ7ÎñŽ{üã ¹ÈGNò’›üä(OyÄIðM˜ôhí¶€Ûüæ8'7rÎóžÿ{ç>ºÐë ô¡ýèê.Za±pxûÞÆiZ ½þX¦°ý1v5dM ®OÃëÑÈ:5—F©¡Â޵رöh´owÆÚß1wkÄÝwoFÞ—Q÷vô½ëÚØû2ŸŒ¿¯ÃðÒ`y5O ÆKñé€|µ'ÉŸÃò”Ïü0_Îkþóuðü8DúÒ¿ôá@½éWŸÕÃõ¬ý`ß ÚËþö]°ý6tûÞ_÷Ù¾ï‡á_ÃøÄOþ_ æ+ÿùEpþ4¤}èS?ì1¯¾ö·p}htû¾ÿ¾Ü³þòSAüÍ@¿ùY¯~¾“ýðoBû•1ÿøk¾þÈÀ¿ý'¯côÿÌöÄ €8k( X€y–€ÀÀþ€ hTè ø€U4¼`h0¨ ˜ÇѸ‚ˆ"h %8‚5q‚´ ‚((,( /Ø‚/ƒ°@ƒ2È6è 9xƒ)±ƒ¬àƒª´½P³Ôz³Rk©Q›µþzºµ\«µCûµ ɳ{H¶b›X{¶¿j¶€È¶jzn ²Tû¶ö·`µÏš¶t[®a»·ÙÚ·~»¶s» x»¬z¸îj·©9¸ˆ+²NÛ¸|ë…k¬‡ ¹˸¹0¹ÁZ¹–K³ŠÛšË«œÛ¹¿0º¤[µŸk›˜{ºrû¸¬+±^ûº»² ´´[»K{»¸;’«{ Ö ° £ŠÚ»»ëœÆK$ -”Ó@€le™ºÇkšÒ›oÑ@1·MÆ™Õ;½YÐÐ<1çb¡{« hüsP¾Ñ›¼Þ;Ÿð+°21w§f¾Ý¿VÐÓÈ Øöþ–óË¿Ћ" LxXp¾³ª‘à Ðüª¦‹ÀNº¿‡ÁªÚJ¡£b!Ûë lª ùÀÂ0àHÌÁ¼k $pÓÆ­I9é)h7dŸêÁ4,¦B¼¿^µ™ÒÃ%@u`À“¡z”äûÛ™—6aMúëÍþL“½¯Æ=pðhpÐŽÚ `je @eÜ[sô¦h ºíbÌApoùqÓÅ7Óèq`ÓÑ;0ÔD]ÔF}Ô@<í· ™³1)\½`T]ÕV}ÕþXm°Ô{«‘!ÐjÔ!Ä‹Á0ÕY}ÖX½Õ9­ÓO’dxö–fÖt­Õ\M·3)@9#]Öu]×jÍÖm»Ö}`bH3)»FÖsý×Y؂ݺ¿àóâÓ0­ØŒÖŽýØ‹ ’ü<Ï‹}ÙVÙš­ºÀÀ§ç×¢Öwý¶ñÚ«mׄ]ÚHðÚ±}Õ¤MÛ‰`Û·]Õ¹­ÛÜÚp Û«ýÛÀ]¼ÝÛ²}ÜÅ9Û~JÜ¢mÜÌ-ÉÝÛÒ=ݼ(Ü[ªÚÊ}ÝØý¡Ú½ÍÐ}ÙÞÿco•=ÝÕ}Ûå-ª8ýݵÞrÐãÍØíÝ¿eÔúÔJíÜE»Þ±}þßUРÜT-ຠàÅÝ“nàË ßØ,¨õý×> nà^Ú Ý ŽáòÝ£NÞþáÝâ/:âö]âžáš­âÎâ îßA ã€-ã'Nã=kãtíâDlâÖâÊã˜ãA®ã8Kägíãmíá-.äªämäì å*嬽«žãá±:á7®åN>ã]ÞÌ’ËÝG®¨@^åH³XŽÛTàVΟo>Úq¾àmî²uîÛwÎáy¾²{~à}NâN²þà:æ\^æº*áO.æk.ç…þ±‡ÎägªèiÎèâ9éô æ=>è+Îé[é ã¢>±¤éþ®é›îèdNÖ‘Žç¬ÉÿêéE®ê¯®é©®æ«Îê»ë½®ësŽ”¶¾ä¥æ³þë‰ë~žìÃÞ”hÎæ¼žëŒ®ìÜËì„îì§Þ ôì×îíení(ŒéÒîëÏÞzÑ.éÓ¾è¾íYìSŽëì^íçÞ×ðžåëžéíîêó>îØêÚÎïúþíÔîõÚàšÿnêÿå O½ ìæîî ›î²ìßåâ®ðßñÿÝÏ}ä®î/ðå¾ì!Ý#ß©%ñûîðò3ïñ¯¿÷çò>ðOñ†™óv¾ó)Oï>Ÿ Ý^ó.ñŸ^ôóÚòßûòÍ~ò2ßïþô,óR½ò õÙ.õµ®õ¢ªô·Þð_oõ[/öÆÎôëêôgöÇÍöaïöÀ ÷øöñ.ç]oé]ãXïÐ@ÏçBoò‰þÞgK÷Îõ¯Øû½øDÝßjkøç‡ø ñT/µoá’/ñ*oö|¯ö;û÷‚ø0Oð•ÿ´—ߥ™¿ôù>ô\{ú;m÷øNù<Ÿµ®ÿãrù°¯óß÷r}û˜ŸûA¿ú‚ÿµµßäÀøÂ?ú­ÏûŠ úˆNú³Oó¥Ï´ÅéÇúÉõb[ýÂzýÏïï¾èÌoÙœ_÷áÿúçOéãŸèÎoéÝŸþÆÿ£¾þÜÛþÇ®ú²Ïú´Oÿãnþÿ¢¯ý@`‰EãqT,MçµT ä›Õn¹]ïÉeóV¯Ùm¥µ—ÏéY%Eš—Ráq%S/°‰oîOP°Nq‘±Ññ2RrÒëòÓ¨B À€i`””¤èî00Ño)UoÕ­Õu¯*Óö7Ww—·Ñ²8nA@ãã`ãH@ä¤äo6 –ÍÐ {M{Û©;xœ¼Üü}ì7]«Tˆa@¹ö <Üž0j?V¿|âÚ4xaBV}|€ˆ¨FD14‚ÏA4ßÌ&+£Ç†#I–4‰pÝÉs  ƒ‘ehâ"5þƒDªáN㙟ۂª4ziÒE)•ö*À€ÈŒxÐáC†pÁØÑ¢7` Ú³iZµkÙÚAÛöÒÓ¨S³ˆÐ`Zµa}š%úV¨_kEá6|X!SÄ“Xºd¦…€¼;ÿñM3t0`3˜g^ütè\ŠE7zQ³‰ÈÓ$p@¯+ÏêwNM†³ìÛ¥y÷ö­fá ìþæ]< èÕ½;yçl1¹SICýõãÛ¹wÇBÚ;›a<s$T‹E¯áÔÒPµSÔKÍoÏLùÙ´ÒR]½“ÔW'd5UWeSÖ\_UW!Œ„TS\ƒå³×b#åµ×_QVÕ€h5ÚQA-UÙþZmòÍ'§½'[F£ý–Ndu­¶þÇk·ð”›m‘@÷ (Á}—@qs%—ËZ³¼•Xxõ%R^YéÓÞ?ñmvß‚cìwWLmuVØ6bvõ_8Öá{#Þ8^u-­X[s;íöS‘ÝztYŽUþobjÖø#”­½8å•mÞ®eQAö–هŒ¹Ü›…>.çPw.™æ™{†yè¦?+úã—f8ߘŸuëÅ žôètM¾èz—ž:ë²ÛÚZÒ®Ûõ8§°c³åV íc¥ŽÛꆩ&x»Pµõùš[·-†»f¿—‘í´ïNüç«÷öyñÊkÜîS•ΘìÈõ¶t”0üñÍ'gº/ÃCuÑ 7þºô 7=ïª[¿ÝÀœ§×`wð¤eÇørt—VóáOïNñd ä±HDC2‰‰Ó" )G#r’=”¤˜(™ÅG°‰™Ô$+(FzÒ’[$Ÿ(g¸I+uÒ‘©eþX‰BWN –dĤCYËÞRJ¹¼ä*¡ØK_r˜Q¦*_XLZS‚Éä× OK>³™ÐŒ 4ƒ´ÌYŽ—ÏÔf¹ $o:œ›!Ç)ÃrþèœáLg’Ö¹¶*¶Ólï¤Q<ÕùF{2rŒøÜ&)×HMv¢òšÌÔ¡@÷§ÏƒôŸ•Lè7yÈPó9Fü¤§?ƒ'ÑFZ”œµ#D;ŠÐbsþ¡ ½¨HIR=z4  Õ¢JWzÏRž±Ÿ8ݨNiZ<Œ¾H£^âèKMÓžnï§# *m†JÈ¢Vó¨5]ÙRÝÔÔ6ž«ž%ªTUFÕûXY%êVË—T‰¹Ô©Ö<©L?IVâ™õC^¥!X)Ö´ºzpõ\ÅS¦ú¯•ÓkÇLyPµ ,è ¾V®W]çPh\³™Õ¬f¥±XÚ´ …èS «Kb†©UíjY ZÏò¦±zl)Ù“ö>¨ÀnyÛ[ßþ¶`ékC[–¡Õ¶`Ãí\—ÛWcW™Ã$rÃJY˜B¥3…n0¥ËIêÖÕºþ¤ía±»]\v÷•ßhy)ZÆœ>×¼ðDïyE«UÓfó½âÜ)|ãûP×¶´¾c½oJå©X¡6·¿û¯"Õ{ÝÒf·­vo‚3:_îø®–eb'|`þRø¬ ž.†“[8Ó6²Õ=1ˆ½cÜðÌÖ+ÆjxÉû`ïÓÂÑ%±Š?\UOöÇ7þ‹[Ü`ñÚWÃåð.…×OsÇà ²r{üÕ)7¹4DîŒg|åu¥XÊUÆò–ŸÜM#×x¼Ö®„™©ºÛí렑ˤB•"Dø<@>h‡ÞÊz»{¶ñm”ÅêñÒÊûgö.„B /xÂGírïùÛ5ùćÜ$¦”ê+8ùÅ4fì)Âð2„´að  Àîyß{ßÿž@f=°ãùÉWþñ=€YQLøÑ¾ð7@üå_ûÍ¿ìó¥ß}ÞS³Àþø“¯}hpßûÝÿe‹O~÷›íwÿø;YùÏÿúðÁð?~ýß¿ÿ’¯þ¬/—ïÿ Ðíþ•OÿÄo‘ïù2«$p Ð#0é$ ÂèD‚  éž®DµRPW[Ð_cPgkÐosPw{ЃP‡Y0èî„冀9Ž æà`挀6à6‹ «Ð ¯ ³P · »Ð ¿ ÃP Ç ËÐ Ï ÓP × ÛÐ ßp³>`LAR ÈãÎCALã4Žã éPN³æßà”cM`à NíÑ#Q'Q 1à eðTËõØ¢"`@NÁïBBȤ?1‡ÀT‹éÈÄ „þF±J^(.#Ŧb ?@óäD`ŽáˆÀ. Ž2ŽÅd‹ŽQÀ A>Ĥ >€Àã„ @™^’C  洛#RH ‰  ³ì"æÄ¥Qé¤ùóQ…€á¥è„àè6±0 净óO [ñLr'eéø°ó"^ %b¯ì¥;! …"M@`‚" ÒÑJNÒô‘&úQN8/R2&†€%]²X*Ïú.p"+4À ùñ…@!KêÈ&3<À* |©Ñšrþœî)£ÅºN(-… #Mr)·òó„àÀ&_2-àèvRN$@%Õ’½ò]`eÏR °rLNR'W20å& `NìÒïLà%èÒzò] ÒŽ®$=ð0Ñ’.1ã²pRNÚ… #׃ßÅåáå&¥,ÏòN`2ç1Rq"E“æòLÚ7‡ 6ò6õe<Êc-Å$€>@b1)Ϥ8`€8@’±:€É$:§³:K` < XÒHœn?€=“. :  ÞZ ÄP0Àk¢’- µÀ!ÓqqþL4@G€`+¬RÐH`µö1 ”/C3TC7”C;ÔC?DCTDG”DKÔDOESTEW”E[”–Q@z!%Ð. \TGƒÁàFÀ?GèS1`Gt¸2  ˜t4½`1‘TJmaIìr€´Ó*wîàA$`žrÀ4fq+TM à¼qp3%1àb±Rò#íRàA ³Tk¦TP#A–@ Úr ` `ãVñãÂñ:`Ôé :Á´+ô±¦òà@`<"¥bÀ=D@*ÎÃ. àÞþ#Œt+ÏÔÕVa…K…`+ ®"ù´RB¤"ý‘1cÒâ”rTBBRr5rYs>pbõV·µrÕU @ì„®î솠-Õc¢cR2”5ÁÔó’!%ëqj”-—µ•[ÿ•> âaWM( PMdUbN=œŽµòqÉuZQR%çeÏ.›qLQ“ad×À[ÁÕ´”@ìL]õÕbn1ëY'v9&¶“a,©õ<Ú’cYÖü5d ¼µ`U–\VX7¯AÔc@‹ 8œ=Lðf-6OÝrDêÍÎh·– 6Hö`ÅNR9€Riêœ܃"@-s9BuT"—Öòª5v¶`êï¹Öo·@hg4\»a‡àK@$Àé4¯F‘ö>ÎmÛôMÃô^é¢rsÖj… c!L aÿta*Ö @3tO× `ã8 – Úuc—" Aùo LWvsWww—w{×wxƒWx‡—x‹×xy“Wy——y›×yŸz£Wz§—z«×z¯{³W{·—{»×{_%!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbblllttt~~~ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏž;:âÓRÑ4 9ê†)΋H:õ €@ƒ‘T¥ t”éÑA£8EâÁ€ DÑDê‘D$Y[f@¥iͬåût"ˆ ˆ/H°ƒ# pá Š X Jˆ k‚©h@BM:MDˆp6&vàmm„ð¿€U—A+¼tàÂèpÄó!#ðÞ|‘oâ.(‚‡>,É],ï"ØÓT7^|LúàÈ;†üD’ŽùÐ@@€ FDü ú áh¦Tj •Z l…€Dø$Bþ|ÀÔ‚ æ÷{ú·@w$XÀî[wEh€ãpçe¸S XØâ °•”\‰+>7`@PÂzNÐÀ[ (’L7>ù a°‰ d|AÀõ  üˆÄVn™AlEY’µ¦f“pÀz ²)"|&¼W °€€§¥NBP®©Ùˆ˜ €@€ ‚¡8[ŽC€™™O`§°a^h! *¥à)Äk„©‘ÆEŠ€gç zèˆ0”2@B©¨©*;³ÎV÷š &Awþh‹¬·hJTÂV¬Q‚°vâEUâ!áÁyn°±F„€V.øêÛ X‚rÐ&ú©¶?™•–NnKqˆšà0œR šÚ€ŸQ½‰„r^¡êÈHH¥˜bžI^¼ÆU,pùÁVb;2Zž ÑcC L° oǨN´f0`E{Œ´ J+ÌðRÖA±¢V/ЮDQ=æÄkfqÁ$”‚šÀxDÈÁ¿I¨y¦ €ù€˜D -Â` åÄ¥§SÆDüݲN­j„—ªÓÚ 9WIЋÄkí&UଓæD¨:¦ãDÌw9´†%Äþß¾ !8£¯íÝ·hÁnÂf³ž4æ“{M|ìcG¤ÜÞID¶´h+Á%Ð-¹ÊE0óbÊ`Â¥QñQ´ªö¨ØDȨ2Ziw\¾á—¿ÊôŠEÄÕ¹¬H€ùÓö yò“±o2ÂOmô' F'ö‚† ,2xG©Íb¶×= DÐÊBmÎóFæƒLžCæó &8/OHXÜ8`ê-,£ Àr= ®jô{Z`5­!,(bÒ2倸ýGzØÐ2#¼ ŠTÉשƒ#\%‘GÜ!®§º<„6‚°®'FáptùZKþM°Æü¡ï8ì)ÂUÈ5qL öáWÆ€%tÀxSBm®G„Šxû£ -´LKbH< Êt6˜ÀD²”ì5¹CßÁUHÀË¬Š°EÃÀ‚½‰bû*æÈIŽiü^‡ð®fpŒJ8ãk9 âQŽ…äãBJàœ1-0DB D£$ì‹`#‰ ·$Ð.pÄ›p 'Dõ¥-¨‚ªwÌÀ̧ˆÆÑeIS&nFQ5“ÐJ1Â=“œêLÀ:ÀÈó‹Ch̾9¼;þ¤›.*ŽB *<ÛÏ’JÔc.•‰²A! –vy„a‚™®þI-mîe`r3A òõ³(¬6Ðz—HSl9´cÒ™ x–Î3šÆ? ó©(MH±3Šéwt¨„~îÆEB½ã6…jdÈÞóJ€SF ¬"ØLM(Ádfi[» )×2QŽ"dy7¢Kµ"°ÁÅÔÎf hV³M¬€7ˆ_>a#0@B` ð,"h§I(÷G!ÎXÝÀ¥Ú)œËBàšmcEGÊõ|N*²¡mp³Qóu1†âŠ’%ÚVa¬YÂJ¤07 .ʪvµ&èSl"þì— • p+;å8À3Ý®]·à0‰ÞC àþŸ YÃuH¸•vFçTÛ9 @pY@íŽpX#ÐI !pÀ[â;q©IOò´2€”à|*âÀ£òÚ©“Åð$à¥Ò3ˆK´2°¹Ô¥¶†»íGw"‚ÇreVÓR‘ØŠÆ"ü×Iëyb~÷k€þ²¸®&À“œ„¿¥ –Gãb’†Fc¼֤ÈÁG€yʵáòhôü:‡GЯñógÝGgÆÐݱôj$O_FÔ“ÑtvþT}ø®FÖ©±ui\]_µØvt”}ìhçÃÙͱö´»Ým'GÜßNw8Ì]w¯»ÞÕwpô}ï€'Ãß½1øÀÞ …çFâÏx,,^o¼ä¥ylT~ò˜gÂå­±ùÌ{Þ§Fè?OúÑ{å¤OýLÿrÔ«þõV`=4dûÃÓÞ·¯=às¯t×ëþ÷Màý2„|·Ÿê¾/¾òAŸ|«7ùÐ?>2¤}LSßׯþŸ³O îkŸÈÞFø¿ÏÑñÃüäW!ú}±þô·«ý¼€¿û‘#]Ôþ8¹?.ôÿšðßÿ×2€´@€ø( x€,±þ€°à€ ˜è &Q¬€8¨ ¸ ñ¨ ‚ Ø$h 'X‚‘‚¤À‚*x.( 1ø‚1ƒ `ƒ48è ;˜ƒуœ„>¸B¨ E8„q„˜ „HXLh OØ„…”@…RøV( Yx…ü°…à…\˜`èc†öP†Œ€†f8j¨m¸†B÷|`'‡p(joˆwX‡Îs{¨‡o—‡†ˆ~hvtHˆ|8ˆÖWˆç ˆˆXŒ8؈x§ˆlG‰’X~–(w™x‰ê·‰“xˆœ~ž‘ŠÝPŠ€€Š¦y£èw­¸ŠO¡Š~ ‹°Èy¯þHx·X‹þ—‹ŠÇ‹º8€¾ÈŠ ø‹cC‹jŒÄèƸ˘Œ¸‡Œ–Ψ͘Õ8Êpw Øx ÜXߨÝ'¶8Œâxá8éxŽçGŽÕ°ŽìÈ~î(zóvWöh‚úØzæ¸1o y ÙYØ³Ç i ¹ùèÏø¹™™‘h‘ÍБé Ù{9’'!’f ’(‰‚%9|/Ù’ Á’‚“2™„69}9y“N¸“Øç“<9…@9Ž'”!8”Ã@“FɃH)~M¹”û ”b •Pi„OÙŽEY•A•`À•Z …þWù ^ù•U–ò˜•d ƒfÙ c™–‘Ж\—nI†ku9—íЖ%‘ I mTCr‰—‹Ð–" *Cm³¤ °j p=ƒI˜‰0˜ã£˜Tˆ‘)™‡0˜$ÀDD昙šYœé—H #x! S'‡–£ùƒw i™ "ðð¬)š¯Iv± ³é" hEÀ›½™Š¿ù ÁéUœÉyœ¤øœhû–ÍIƒ ° ÐuÐÙQÜÉt ËÉÃùAÆùÇèšÅÐ # Àà7·µÀ€ºéœì©ž—o4tñ7¤ð5ÿ4þTÒéŸÛžSà  º ºý¡BY¡§8¡ZŽñ¡ªŽª€!ú¡§‡¡½h¢$•#Z‘(š¢b¸¢¯à¡.ê2ú5:£ £¨£8Š|-š 7Ú£lÁ£ù£B*AªyDz¤DÙ¡KʤNi¤×¤P:•Oš TZ¥]y¥#È¥Zj—RÊ¡_JYše:¦«ç¥.¦hºˆjZ gÚ¦Ž÷¦-H§r: qÊ|lz§ã§üɧXh§2(¨€º£{: ~Z¨P¨C¨Šº ŒÚšª¢‡Z¢“ª‘ª •z©@ꨟ©œšsžÊ”›ªbú jªØ9ªªª*©qXª¯þj©±:«lȪAˆ«¶š«²z‘»ê¤½’ºú«™©®úªÆ:¬Äz ɬËê£Àú¬y©¬KH­Òª…Ö –Îz­ÅЬÜ:‡ÛšÙú­à­äê¦á ­çZ‰éª“íº®`­ðª‰ïú“õ:¯û7®ey¯ø €ú: ǪªæÚ¯®È¯Iù¯ë›¥ [°òÚ°º°Á°¦:°Û û–{±ëù°Û©‹•‹±!+–;²x`±(ûŽ'K—%»²pÚ²å*³0+*[³þè±8›³µº³ˆJ³… ´>Ë7;´&©³F{´=›´¾º´L«´¨*´O»’R‹‡U;µcP´XÛ­þWˆ]»µ_ µ`˰N;¶\û²lùµf›¦h¯kk¯Hû¶"[¶r;·QÛ¶uë²q›·n{·|K¶~û·vËtj+¸”W¸ k¸&‹·ö‡¸Šëbû¸º·’;¹t[¹µ%€   Vʸ˜›¸¿@ÂrÞã'Cµ¢;ºÈÙº†àÞƒrg5 »®Ûë(-ƒrL¬›»é¸O2>ÞÓÀ[ªëX.Pä=`»YK¼Â»ª¸K0>!Pj¡{½™k½ƒÔ—qS7§ÙK¾Ú¾„°° àaмœ¹î‹¥æ«»ý{½ëè~q>IaþO÷û¿Â»Žà ìUðt»û+¢ð{$%L°—}I›B0^8÷§̢ǰÔÛ°jÂÁóâdÐ#³‚¿—‘¿ûYc†zÂ!l¨ÆP PÂâå™ gØÖ¨¼Ã‰ÀI°°N¬+Å5œpjvÓºŽMŒ\ŒÂ&·åYﲺXÓ[FŒÄá/Žâ¡°Ù®ã^ã>N®8^Ón£;îâŸ}ä´MãÚmãséäàm¥CåEþ­TŽÞVä=ŽÚ<~ÀWNÞYέ[žä»ä'þåq(ã .ä^ÞäR^“nþàPNælµu~áwÎÞe~­gÞçôýçÒèpæ—mè]Žè‘­èbç‰>çÕ»ç~èL鄾 @Îèq=æ~žç„Ké%nék.ç™n•¢¾â‹þ~é.éVšê9¾ê¥Žé`Îêé­~êŪ۶¾ÐjNä >­°Žä‚¾ßºþ«Ž¾¥ž>èÁî|Ãþä¤ì¦^볎ÎËnì;›ÞëqùëXžísøìUþèœ.ØÉní¸¾Øç~ëå~×ëîë×>ãǾ«ïÞíñþæàžÛ^íìÎíj]ïizïv>ï¶ ðZÐâünî®>Ôéï ïî _ÖðßÕŸ/í´ÞæñÞŽçÓÎñíNñ#oñÎâÎåäîï&Oð•°ïî%Ñ?§Ïç.¬'×)æÁ÷ñŸòzÞñ6OôJ½ó¾ÞóÅ.ïùNˆJíßÃ=lþÅ­ÞHßíO/ë1×ÌÍÓWŸ¦Y¿ò õÔ]ö#mÝ+ýõÏëcOò,ïñ7_éiŸóËö[ïð3_ôyŸ°5¯Ü@Ïìb÷¬­ööÊnôÑ÷£>÷Ô.ønßök¯øªîõt­†/óo¯÷™Ï÷„?§—ßï÷ˆ/Þï÷Ÿ÷›Ÿø£oõ•¯±§oï«ÿ í”ßøQø{¯ú¹Oú­ÿ…vû˜úš/üœßûóÀúŽù±?â¥ݯÿøË/ú»Ïú¶ò¸ŸúÜ=ûL_û"¯ýTñÉúÄÝÏ¿ýÑÏüÕ/û’ëÞO %þa ¶Ia!˜¿ýÁ?ýÃÏþÿÅo @°h>ÀÆ”Tš4 è•–&€bÑn¹]¯¥UÉe³ùZù®¿áñW¦Ùõ­[ž×ïù}ÿ0Pp°Ðð1Qq‘‘ìê­1Rrò¬ãHia ¬ ²ì*Ë®¯‘.”m”±Ô´MŒÒõ6Vv–¶ÖöÖ±w—×äA`‰àƒlC ku uQU™‹YÑùY+º÷;[{›»ûðÑ;\ñà`) ƒ¬i@ !$”úî¸@mž¾Sú>__`@ Î`Â8,:|È0 ¼%ŸüU«ÇŸ?kˆ¦Q©eJ•+YBØ¥†JF„#"@ƒþ*;žtÙ¯çÇD%Ÿù„yiR¥_.ˆ œ’sä0¶1YÐ}CŠJ²k>£NÉ–5{–ÔW´ÙXbl+«;Hp7¼UcUÆWß½j×6|ñ‹ Œø–%˜4ÅÁ©«Þy€ 6Åù`ØÍ!—6}ºeSÔ¸„hðЉ/#’,ÀÀÁŒçdõ÷›h“¤û /J|urå˳©f.« LNb„¶‰à@D¼Ž`w6þ·|èbÓ?wÿ¾=àñÅñüÉs(ÐòÛÙOÀ„ù»±½ù éO”öü`ð œÂ÷ ¬þGS—‘YáÃMLîÂqÑp8;o0Í[oCm¼qµqœ…ÅãdTo¼ÿúÑ !w<É„tLÒ•Ñû‘??q"»0’É,µÜfÉ-ÓÒ¬Eü¤ÔJ=¬„¦L/Õ\S–.Ù LŤ1Ì´è”óÍ<õäÅÍ=‹‹óÉ9ƒLSŽ3ÿñÑD›$TÑ3œŒQP­"½¯ÑJ-ý ÊK õ-ÁI;µsÐL5UÓ>IãÑ)E qLÿ%°ÕW=•V?M­ÕNkUR^)ÅØSo ¶7@!õõÓTbpVbMrXgS%³Ù< õ¨ÚMï ôÙn–Øi]ÍþÕe?$Ömõ–Ý<Á V\Y=ÝUYuUm_6ß6^fç­³ÞPóxË}qí÷Üñ ¸W‚>ÒàZQanþõáŒUŒ˜Ö‰‹|ÕQs)F–^MÆcauØ{©­xÝ“e60eR=¾dd\æ{gþYÀšG½MtCÞY^’Yši÷„.uå…[Xi©›¾Z¹§/%úЋ“ºa¬ÅFMkK¹Æ¶ç—«¶xì¶!+»Ò³É3Zgª½.Ùí¼×‚»Q¹±4SäéÎiõ>-¾õ;gÂícÄ%WJño£f¤Ç3{òÎaªQÆÏ¥ð„׎ÙóÔUÝÖËQçþªô‘ï^ZõÚ b}OÑÓæùtŸmÿ½ ÜõÔ½wµg·øäÃÞ]×}‡üëÍ#Wžz.ßÚyã¡ÇvÍ«ÿþæß$þxÌ»çüôW¼Þììy/ÿu°b\ýúw__÷“†ÿy飷€°Àߚȷ=ÚÉÏ{T $¨¦ú{Dß)(Ÿ™=ð|Ó“ +ØAL]P†ãŸö x@žðE£kŸ±ú—Áÿmð…(”áƒØ·šn„ï3 òfØÃ*Õ°o7”ÝÍÃú‰°R¡ YHBñ'óÃÙ“X¿z ƒF4aXE/b…Š‹"ýЧÃòð‹i¼bÁÆ8ÅÝíþˆñKãר¥,GŠE{£çØÇ:féŽ\œ »ØÇ$þ‘I\PàÜXF8’Ž@cÍøÄ-.2]ƒd$Ãh9J>Ò’h$7ÙCDB«zt$ÏXÄRšR’ž,$&gÙ!Fªò•U<%’iËL¢m•8Ì%w ±Tj²•re ‡yÂbÅùåÜö(ÌfÎð™8Šæ‹¦ù7kÝ™×Da6o´ÍÕRšègÉi#siË„â:ØÎ˜9¥EIOÚóDïŒR7cy-júsœ± >ªOV†Ò•e§B[÷ɇò3¢´Œ§DíP ”†í¤A½ÉQÕy´þD 8óÇš€(ECKZ.‘Vsˆ0h'jQkBT™%¥NÁ'Ó ©”U7 fN‰jEпަeiC]êĦ¦Ï¨Bê•êП^µ¨O^TG:Õš¦K`µ]V'´UÀuµª•TkõØJ3²â”Œ^e*"J ¿þ° ,æjCžV4­ðª/%<²‘•ìd#{ØÂ¨®rë7ášÌ—‡íhI[ZÓV€b½l|2´».5¯qÝ'!7ºZªv|¯Õkl=kUÚ.Ö¶l´ìX}º×ß2S¨ÈelmƒkGÜæ¯¸¼êgåyÉå·¹‰|.u+Û‹wžþ×Unvµ;ÜæE·‘»M¯w¿ ^ë’•æÍ-zqÉ^ãV·Ÿâ /|y¹ÝÛÒ7œ ª~ßËÍÆò÷žò…îWÚÙéú¿E°1Ì]·´·ruo~ÓÉÜ —Ó¿Xìn†gáw»N0/LU×ÄÔMî~U¼b™mV[ NªŽ¹Êãk6ÄllñYæcÎ9ÇþqJƒìÜ!KÕÁ–19¼d&Wø¿H¶©–ѪdÅŽ×Ê3ŲˆŸ\Ö(oXÂ(s˜µÚd@ŽøÁŽ1„g\`6·ÕÍÚ-3^×ûâöÎYÎw3‹¹\d/´Ðu;´ ÔZÌÂYÊt¦rš ¼hF³6ϨþÜ3lûç׹ʗ~ô˜…œhÇYzǨEÍG7Òh>q¥Süe·š>¯Îu¬ýéIÏúœ¬Æ5Š2ÝßM«·¾~¾/¨)=løèÓǶ¯t#è_OÙÙ˜&µ“MMºnÓå̽†q¶w½mæD#Õ9 ~x WÒV6µemZ¯™Ü®.öjZS„K¡À<À€À›.òöô¸¯-i5ß:ßËöQ,d¢ ÐEÀ a'¹Öˆ›ÅÅýg´\S(oŸ¹•Ó–` ƒ 0À00›„+áfvÌfRo‡—¥ç£ :öXžœt\*N ƒ;–` „ ç½ÑþyÃu8tªûÚ,V·”ÖƒXôÕ0Ä!!C °„À:Pÿø–×ÞeŒq=Ø!W ÜE÷…z52 »M”P²›í7o»¡åÞàÇÙ=¤QâýÄø±âý4ä0G¨R…`& N¯ÂÙ@Ïô¡=&p„¿z`©WýêYßzÕ{à¯Æ˜Àèi?úÒoàô®×ýîaïWÙ×øŸ¿ý_°{ã³¾÷Rø}ð?|¿¢þøÑO¾ }ãw°Õ·¾î§‚lßøÝ×>øYýÜ“ßõâGú³¿þÖw¿øî_½úåÿzÀv þö?þéÏÿØÃ¼R Ë„ª@æhÎþæÞ‚²Ð#P'+Ð/3P7;Ð?CPGKÐO#PåüD2(#ã6În H`¢@°nsPw{ЃP‡‹Ð “P — ›Ð Ÿ £P ë6€.e(ï5þ­-²#à®àà#Ž 'ÝàÒÎÐØm:Þ­ ãPçëÐï°ÎPÝ*åì$ ò £"@@v" s"<Ø$±•` #kæØÄ€;€:±1_úÍVPO<"lð0OD@À.` pâÝ8@à®pMPþQ€• - @ xCM2à8à. BÀÝ<@Û¥kcLÑOD1í*…lq @k ô¤o1 À-üdM R1³1 p¢ñÅå”@þ1Înåûîï0!ßäq+%ý®ìô1_$/é*/QDq:`ÆQúÑD– 0O ÒÄQ`  óÄïtB"ÓA *ŸEïhBì'*B žŽ­1 è±ïj.;Ø#3<`"¨ã$פ` lR p_ `önTH`¢â!iÒŒ2 ’ ñ „ïÞDþ&r*ï(à#Ÿ*ÌAéF (uR*G’"-ò*Á± Àô¤+ùîH2 LÒÿ±PSÚb-×"r ð"¥r.ëòM.@1M kÃXšÑ 24Å)¡r&嵑½ñ1_N/ÕÄ1ñÕ1"5ÛÅ*\ÃêÑO@8à^2&¥8  €8€ pb: bñMv³ à7K`~Ñ: "'“Mjî£àЉÓ8ó%:¦c†±@ £"+0»cóä<%kH  = @(I€²æ@9ÿ@T@”@ Ôþ@ATA”AÔAB#TB'”B+”M6€;  $«à:ÀBG” H à:G <ûÆr.€Dc ¨Ò ðl´ pT èRF{”ñ± +•À;` €ä>`@š '7ºc AC ; @I½5%.€>;@"+¯+C`(àdQ²–ÒGÛtD  0BÀ1• I=`CЫ£0skîˆ3J5B  'áM < -R"`pC"6Ó. 7F­àDwÑMCõ¤1 †4 0  S‡ra2<þb Íñ;Âñß’À1Dô: <$r#òßîR pG;UT‹•HÕê4 @Mf‚é”À1iƒ’€ °ñ:Ôr ¢ð@"Ûq8± d‘S]eÁ(MÕÀN €MÏU 4Ž6jn?3Ò//®WÝÒK[ñ,»r4 ä5] v< ¬ƒ]-q[‘N‚Î1ÃCãÌï¯Â1_¹U D jÂ_/U,•@`!“X –dY6èà5 XÕZ¹ƒ6úµ ı nmŽc•`LKõc“À²C³dƒ> auÖNSéú”þô<³£æ 78 nñ1AÕQ§RW2 ,µT `ÇÕVûxQhÏöUYÛ5eÙt*£T" æ°ñò®jmUK—´K¹ÖW=6l³#¢ÜmWÓq7! €à8`C²²Ôq/× "€>ÑðCå€q1tCWtG—tK×tOuSWuW—u[×u_vcWvg—vk×vowsWww—w{×wxƒWx‡—x‹÷Â!ùd, X… 1"""+++444<<<MiCCCLLLTTT]]]bbblllttt}}}ƒ¤Íÿ‚‚‚‹‹‹”””¤¤¤«««´´´¼¼¼ÃÃÃÍÍÍÔÔÔÞÞÞâââëëëôôôÿÿÿþ@“pH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›´Dx D'þÏžŧ%£i ‚ÔMSœ5˜J•* ° Ã`@‰@H›"±SÊS4 k ˆ&S "ÉÛ2 0m{æí_¨C\X,añ –D0‚„$ €àèû§„ˆ»&šš>›š0Ó¨ÓLÀ B„Ð80Ékdol#‡ÇüÓ Ûâ@‘#Öˆ€×$v– 2]Hõ!‚~¹.&øñ?Ù „ Â!€Kx)àw‘ïi†'GŸørŽ!(+)!@ðqBb ðkCd`ÀV Ô ,5_kÅéD”O!þ0gá“YðASN8Dþµæ‰Z-0 ˆu@ˆHèÀxEHewF¨Ga{~íB"€ã ð¡pçÓ%ð\‚ VV‚|1 V•Ŧ!Œ  2¸¥…ÙUÀ’yÀ•÷I„Jt@%t€Aè)q$±‚ f€„T8A”Åvgep—^°^‹k¹– °ÀeæÙš“DðM}€¦¶È ð¨h!X àvÚ±åyJôz{¿1k­¤¾ùeD`¨×±*»~¸ä¤ÑZÊe}&D×è­ ¦Óµþ8`Þ¶ÐVz©ÜicãY[k¶,YçCü™DvLKÕ¡E` X(W„ ºÁ@e@l6 „«°VBs-fZÄ·„©òT  Õé„§}BxL¡…­:½BÀ$6ñ˜¢GðZhS]ИC›—,P{ZÂXÓ}°‚ ÈŒ4¬&è™ kÌ1Ë©01„X3Õ^íªZ «kSƒ½À¾m w`%„ðØÛD@•’Kt€k@ŒÄdtšét8;Æ¿ú1§#ŸN(Ѹ@ À]}®R-‹ƒµh[pßFL«/“匄þ›núZdæCð çæDäG¹é& €Ã9åÅÝ™øâ§«ýñð&Oä=5Z„NÀ 7DÍ%nĶ[·W DÀÁ´Æ'Á%¬ñ¡=´Í`Âá[ç|Ë’Aú…"ûɹz”´æ]k.ÈĦ¿|ÌHpÝá#4¡I5BZ_ŠÄ:!ÈÍf¡!²°¡9B˜ÖoJ°¿. ¡9_cê×¾B{¶Â5 ØÐNÚžCò³$Lƚό0‚±0`ŸF;b±Hý‰œè‚‡¼ ¹PC Q¸ºÉ fŒBÕzŠH„Çp®gRsS þ2(ß$-h¬Z[®x ºpI! ?³¦°§W´CÏ[Ph‚ia±‹"bq(D†Üi‹GÊ…0™ŸaZˆ:B À€÷ÁoH!uZè* ! P:h¹•ì^le`ÙC¨1VHÈÎGøº"Ð1x!p~9©€0™Cå[y??Z‡‡^ )›§Fª’-4Œ$ƒXI…ð'wÔO`*#he›UkØJ™å%AzÇB~ƒ=–ɲy©#¯Þ(Îâäý¸|IËÿ-sGFˆ‹EÉõX“‰¢áŽî¢§ÄH®­›”ÄçÅÂé{æH’Ô)ÖZþˆÇë³¢å<ˆÜ6É !jKÔSæDÊìÐra BhÚ;¯ÙÔ%”Ó2fд¨cAM ¢³–þñS4ÁÒÊM—看$‡ÀÌ:Ù1ÀQËC,ŠêÅ_GPÙ-µÊ´•2©òä")y69•k!ðš2“êÅ`¤h³ÎÖôGËiÂ4¦éÞ[3‚½,àV¹V÷U±§\IøÀ~3TSnÀ<µÈ2nIë]DTe P€°@²>ŸB—ºØÙ˧Ȩm…z R “ŒmpÓÎÙ=vtp]&­.»[SM†RÓ2\aM@M¸Ö—*$ÛN«þ\Õ²¶yyd$G± Û!4ÇÏ^!Ëw+ °Hà­ŽDD²Š@ˆEùÕ'hü&€DI £5 ·«Ãî¶Î» ð޾ëýï|ð»9øÂÛðä@¼áÅ‹ÃñŒ¼ ÊKþòd°¼74ùÎ{óܽçGÑkÃô¤O½P Ö«þõLp½5dûÚöÔÀ½íw¯{iô~÷°ÿ}Ñ}üâ—žøcG¾ñ—¿zå·ÝùÌ~ì¡wêKÿú··~ßµýî ÿßïþâÃuî‹_úägFúϯ÷õ‡Ýüì_¾û“1ÿø³ºþk‡¿ýcôÿZöÄ €S( X€B”€ÀÀþ€ ¸/è ø€Ë1¼`ˆ¨ ˜5Ѹ‚("h %8‚/q‚´ ‚(È,( /Ø‚)ƒ°@ƒ2h6è 9xƒ#±ƒ¬àƒ<@¨ C„Q„¨€„F¨Jh M¸„ñ„¤ …PHT( WX…‘… À…Zè^è aø… 1†œ`†dˆh¨ k˜†ц˜‡n(rh u8‡ÿp‡” ‡xÈ|( ؇ùˆ@ˆ‚h†è‰xˆó°ˆŒàˆŒ˜uú7‰Éwtχ‰–Xk•˜¸‰Õ§‰¡Š÷‰‡`Ф8x“øx«˜ŠÛƒŠ…‹®ÈŠ¢˜²þ8‹•׊¹X‹¸h€ºø ·Ø‹ÝŒ@ŒÂxz¿¸yÉxŒ6aŒàŒÌ8{ËzÓ$XÈÈ‹ÖX'ÐØݸÑð{ Žàè ä˜çXŽËŽwÀŽêˆ îXñøŽÅ0s`ôˆ€Ø˜ ø˜ ¸­þXýØx9=xÕP¹ éÙ!¨¹G‘y„é{y‘L¸‘çéÉ#’0è‘à‡’&i…*Y~ ¹’*Q’“×’0 2™7Y“«“gÀ“:™„4¹ŽAù“e8”ôg”D©†H™/™” Ù””¸”NI>YU9•¡p•c •X)†RYþ_Ù•y–ÃÀ•b¹ f iy–qH–ú•lÙ‘pk—•P—]€—v ˆn z¹—…Ø—ÿ8—€É’„ Œ‚Y˜òð—ZÀ˜Š¹xY*+síV¡ä˜é‰‰) "P+ƒnB ¿†7^Ò™›ÙŒÉB¢iRÁ‹š¹š†À˜$Ð?–©}³I›„`›•iR±vTB°›¼¹wª9 ·ùš ð ÄiœÇ ¾ùš×ci©y˜Õ™×ù«µÝ¹¶§áI‚ О @wã™óäž4t Ëé" ê™œñ•Ü) ÀRÑÀŒã/ €þà âÙŸQÈŸŸpHUqƒÒò!ð"Ù÷ŸJ‡š Ôù¡x0¢Íç¡$:–(š)j˜ñ`¢-z!:‘+£ú£P€£6ú:ê=º£$9£·ð£@:“5GZ¤ô@¤KÀ¤JjNšQú¤[)¤&h¥Tš’I*[𥗸¢Xê¥ê¦³0¥búydz’]z¦£(‰kʦÛ÷¦)§pZfÚ¡uJ•i wš§WЧûI§~J—{Zƒ…:¨¿¨&s¨ˆÚ ŠZœŒÚ¨©?H©’J£`*¨—º Úsšº©üh©D(ª Z¦¤ ”ŸZª\š©ª:ˆ§z ÚªOþ÷ªNH«²º“¶:…¹z«¨Êª¼º¤»Š…Áú«ºšªcj¬Äê’¾š¬nº¬Ìú¥Íú¬ïЩ±š¬Ô:¬Òê•Èú~Ûš­þ‡­ŸP­Äz­Ýê­®Új®™è¬êzäÚ®m:­è ¯w9¯gh¯ôÊ—åÊ”ùê®øÊ†ÿÚ¯Š°m¹¯Ë{ âú«ïz°þÉ®«Œ –±µÐ°‹˜[– ›±±Ø±õº±k¨"ë— ;²Åx²“°°¼Š±(Ë¢Ñú²0+¯%+³£Z³‰ª²6;Ž: ,{«.»³ Ù³‰³B[ A{´s ±Jû‘LÛ´æH´ð³²š´PµFë¨R{µþ™µ¸µ\ ‘`ûˆc¶k`µf{”^;©k›¶û´nû­m+¢e·PZ·œ9·v»²x‹TÛªh»·«·CÚ·‚«–†[›‰{¸y¹¸½é¸ŒÛ˜‹œ„¹d[¹;¹–k»¹_‹¹+¨¹ž;;ºt º¦Šº¦;¥»ºWªº|*º®ë£²ë«ªýXРРL™W»³Û¤Â»$0-~c?jÁ »ÃëÅ«5b?§êּϫµÎëðFç‡Õ“Ñ›½ê `†T×á{·ÛK¾%:¾+w×[¥í뾇¿GpÐ, ¹&·[þªøXÁ`s#¾÷‹¿tPà ÐðIÀúë¾­ëÀ•ÚÀ¸ÊÁܵÀÁ"J!caPÀ Šà+ì«Uyd¿üºÆ@fÂ’I™ØÉ_NW¾3¬¦Ç°õ «sIIpsd†ç¨?ºüàû·yž§_7Wq‹úÄMl¤ÆP 0Äû…›F€h3](¼©ø¸°lL+ŽÓSœ€FšÆÄ[»ZlkŒ|¼ÀsJpŸH0Ó#hšÇwÌ¾Ø ÈG€3…Ìpg|©ß‰°kúÉ’3·Éà3çÉ€¼_æi(ê¦hûÉžîYÁ‰Ü óÙžõþ¹ ò&Åb\VLq>¼ÊÜ ÐËü˽ìÉM Z *U8§Ä.‡Éª—JÊtýU&ÅÄ̈šÁºœ•l$Àõ+¬0ŒÈÙ|ÂÛL# ”1YÄÀå¼ÁÀàìƒgº;Ä)Ãï|³ÀPºás`gæLÎùܸ-&âK8Åî<нú  ð3=ŠÌаzι£€=¼øºh ²h_ WmofÑ” GUå¼ÿVÐ p0Ów;ðÓ@ÔB=Ô@!-´ø˜¸áÞc•@ÕR=ÕT]ÕPþG½³F¼U ÕV=ÖUÕ6=ÐÙ'H‚cæ,ÖdýÖWÕ6‘"~Òn ×cmÖ.Ý©»ÍÑ(›†¸y­×e-×2‹°[:ÝÒ[؆=Õ|Ý×yÔ¢÷,¾ÙQ=Ù”·Àà¿<úÔœmÕžýÙ§ˆØb»Ùœ}Ú¨ý±gÝ…¤]ÚTíگͺªÝÂÚ‘mÛ·²±®³MÛRíÛ¿ýŒ¹¤¼mØÆ}ÜÐÜ^¹ÜzÝÜÎͳÐÝ »MÜ’Ü{­Ò ×Ô]ÝèÈÝ3ùÝoÞWPØæØ}íÝÚ]Ü 5-Þv,¯æMÖè͹OMÔüMÔF}ÝæêþÞïׂ 0àNßY ¦÷½×ñ >àùMÙþÞNŽàîÒ®ÝÞ|náäM›NÜ.á#¾š%NÛ'ž£®á+¾™-^Ú/þ1®âî­5ÞÚâ>ãÙã½ýã2¾ãÙJäÌmä:®à‡ÌàGnÁ@nâB®˜J>ÝL.âH.­WÞYä[þ¬]~Þ_NåaάcŽßeîâU^˜iîàžãZîäž åMnÎSÎægn­mÖQŽç®àonÚknã}¾—ƒ~Øqžç†¾çãzèmè)-ç`Nç‰^Û…îãŽÎ°NØ’Ž¦Œ®é–Þé“Þà„¾èŸ^Ý—þ¾Ý¨~ç‚NêhjêŠ.å©îܫ߭>çN~ëéE¾é- ë]ë[âºþêÀΗ²Žé¹^é».ì2ÜÎŽ¸¡þ룞ì>+ífNë®Nß¼žà€Þíâýí7N»Õ¾äظÐ^ìÚ®çÜ~ì޾ì¬þîÓïé>µíÞèö¾íÏžïKï¸Þïîþïvï Nì¿Mî×~ð÷žðâ®êòžÙ=ñÑ~îXð)\ñÇ'ð½ÞìþŽì?ò“®ñ^Îñhìñºï¢îOñ*_Ù îÔ®ð·Íð"_ð$/‰5_îM`ì/ó%Ïó'ó¯­óÏïïóHßò(Oæ3ŸÉþ,¯ß?ßðÿðNñìõj>õÍ\õUpñ1ŸñOÿÙJóCoëbá.oíjoòãÞö¤ûö÷F?÷`ï·vŸõxÏô=oßg?ö”.÷D¿õeßõƒOê}mì}°ió\/¹ó­´‘ô“oñûÝßœÿÓÿÝ´—/ñ‰/¹^ç O÷Íwõ;ø˜?ú#ú[ú§¾ô/´°oö™|²?ë–ú(Þ÷)Oûp/ù®ßݾãª/üwOükÿ²·¯ø¹õ‹ùÇãÉÿ÷µßúͲÏOúÓâß߯ݯù»ÏìÊï÷¶_ý´{ýÌoø¸_ü;þºþ0^þõÞû{ŸÚì¯ýîþýðo±@€LEã™T.™Mç•N©Õ¤bÑn¹]¯¥2´J…•ïù&Ž¡eô{«fÏéuûŸ×ïù}ÿ0ðIhMÐð‘ NÎÏͱ22M,1Ss“³Óó4tS´ÔÔjÑò²¯R•k²Àìµ+ö7Ww—·×÷ˆðWØ®b À€ci Ù™)µ6ŽT–vZëVÏuZ{ø<\|\8˜ü|iA ãã`CI@ä¤$ ;»z[-ox¾ˆaB… ¶ù×\‡wF ˆ÷ؾ~þXœµ F‚7„xeJ•§Ì­ö@Àf’0ë¨_?þ“u ªÚI§§¥Ÿ.‰5zT‘H¤¢8@†$òÐ Ä%•Ú ihÒkØÂ.5{íÊ–iE`pdD I C£Ñ' =ܑDž:ìQi ›‘¾mÄ B •\R¥™4DÈÞˆÌmÊ$Ÿ¼KrœÌò‘­;’:…ÒÑ¡"Ç“Ë4ÕädË5óˆRÆ.Í”Í*$³Æ*¿tH? =ôO2eP# •RN*”RÂꬴ‰FÏ|4NJĤÓPEåÒQ4R+­‘ÔÔVMmÓÕ%45UT=s4EcÝUIXy…ÏÖUUõWëJýÙ}MvÖ\âÎU8 4Ùjõ\ÙfÖÑZ µ\.±ýUþÛütÍôSg¹Ý4Üv¯—×r'|¶LV… ÖÝ|y„wWyñ<—Ñt·õR} ößXý•–àNïýöàˆûKØÕ…½È³ÂbÍ¥wç•dþ(nÕb[–Uà×¥5ä–‹ùU/!&Ö^š‡uçÓ`µdXNV"Ú‹ÖÊã9Îé”vµgjŽF·h†o­9éªÙZ:Ô¦9ZY݆©µì´°ÆTëž8j¡ÏF9m“×npÆ®´lŒ¡Mùc¯»›ï¢æ¦´î¡õiÛç·‰¦ºïÄ!ú{ÒÀ ñÝ"Wœr„_Ôq®žúæÊ=?çrD3×›]ÎñýuqB?ttÓgÞþo£SŸ}œÕ mýá‚sw˜öÞ}±ÝOÜm>ýÍØ¥öù]€çSxØ wZs•“Ÿ>—å÷l¾øç·&eê½ÅúkeÖ}ø×³Ÿüûô ßMì%ïü}âÕŸJÁ1ŸwçÑŸoúýïdî»CÐܽ¼ý‰zÜýbôµÝ9°|äKà‹·@Ñá‚úƒß§6 ~vd÷öÀF0 TagDx;–΄0DaWXà µ0x/ìÞ O¨AùÙˆÃ!ótØ5×Ið| â32Äëqs1Ü¡ÍÇD+}k W:X@îñŠaÄ¢ý.ØÀ&ñ‡\ÔžÙÄØFdþQM[äŸç˜F7®Ži’#aÖh·zÑñŽ Ì£¸ (½#¦0~U $É8B3ÊŠHL¤$©ÂAfi7êc#ߨÅÂU’‰—ÄR&‘dG>ö”å» y@)‚‘‡L¥ 9™ÃGNŠT#*gI¿U>‰”aÚd øÉ^Öð—L &vx)Ìfó{É\Ò2íäIè}1ŠÐ¤ 4{ÕJÙaӯ̦6ÈM Q“ żæ!iHNÿ™SYÞ<^,q9IDºs~ðÜ—<=NWÒ–ø|g-‰xˀ撒»¤@§§Ï¡ó>ÖÜ;ÏÈÐô9aüô"EeiÏvZÔ{ÝD3öÌjþ¤ÿ©†Hz7”úó›)Í'AŸhÐq"ôž 5¥Li·Ò µô;-¥.yÚ;Ÿ¨3i:%ÊÆ¢^”¦â³i8zSVô©Fjû4jLŽÖ“ƒ/Í*õŽ:±®®SœTdNÇŠº²Šì¬Më?×úѶºu«ZŒ«S¿zЫvô®xu¢T—Q±Î5¦Õê`¹:UºâÔ®u$ªb÷Öý$Õ ê”kUÕJÙÏYÖ<˜…ZaKºÐݵ©UíjW›Ïr•±zulb9ûØ¿‚•‹ÎÐínyÛÛØ¾V1 õŽhÑFZ—×!¨Àr™Û\ç>·È+p#Üî—mÈUªiÉ]êŽtºþzÜ«·+Ôîš—™ÞýîOÃKÈÙγ®XÕéd5©ÝõrǺø/'5ËWIJò½ïCÛ‹Éý³©äÍl‚ùËàߨ,ù}Ù HÛøv¾ …0xì^ûŽV½L=lm-¼á}vØÀïíg_­Ö7Ä&Ö/ŠGIa˜ø¶~uqŒe, ûØÆ,îlŽ[œa¶ö®4f¥Š7úß;¹É$Æ1’‘Zà3Ù«Pβ”ŸÌå(S™¥V^ò‹³Ëããš9¨ôóŒC†] 9¸ÂS6rd×|]13ÈZF«—·|aÜÖÙÎJÆ3–÷ìgÏYÀ§D3 óãà¸ùp‹^ðˆ ]dFOXÐÊþÌ3Ÿ ` KV×~ô5MèÍVzÈ;FïIÉ,ê“zš›FµmU­æóÚÚÕþu7Mí_NŸÚÓVôªsÝè]ŸSÖÁ>4¨\_IûÕ™Žu¯ bb¶Õ‘¾6´¯vìxR»Á”V¶¥‡knW×Ûû7‚ÅMäT#úÓç.£iÈ=ÆÛ>³¾å½zÿÆÞiw­Cíl~÷û(ÿ^MÀÐßjçÛÜ"Î6ÂQ¢p†ãÉ` `xàÍëŽs—g]bwÓšâÆ–öbÔŽ‰$¡À<À€€üTÇ6¾Ë|ðIO<å ±¸BÐc„õ(!˜ˆ9`ô†ÃùÆO.þÐÉM~”©cêê€KwZŽs€&HX€Ž€å¼àCMû­uªË¹(nǔܵ¾òÄ€é¯ @lŽp•#HçOyÔ¿<îpÒÙtv â'Åø Ú1n e’á Ñ/d”'¾„ŽW{³W"úC™>‡g d&_#”Àò˜×<ÚIÏöÚ§7X¨g5ÏÉ¡{>ù^|ªOKSž•$ à=E<2¯ <úÑ—þô)0w¤ÖÐþö¹ß}ïoß©eƨ_~ê[ØÿþúÙ~ÔŽßüñ‡>úS‹öß¿ûî¿üåú£6ûðOõïPï¯TËþpý€ïÏ0»/Õ¯¿o3P½Ïíï¹oGüT«Lð1PK°Åì&þ.쎀ÈÎÌîDŒ€z ƒP‡‹Ð “P — ›Ð Ÿ £P § «Ð ¯ ³P ·p³ÎOŠN=Ø ’.ì–. H`ìµÖ ÛÐ ßãPçëÐïóP÷ûÐÿQ‘ ÑYë6&E@<À ä8N$æf®æàƒADDäHÄή8ND>.KÑOSQe D2nó %óvKøŒþDÔ$ `EàEÔ¤ààuKÓÄ@E€Xt3z1\ZîÅ(C ?ÀµôD`ÜáŽà2>Ždn¹d»¾ÑŽ£îa/Ò¤€8àn®BÀã<` « Màè¥_ñPH  Àµâ"ôÄ Õ±€‹ ¹¤K€#R!‹à2\º®cðP2 å!_/öŒ®Õä$@")eìLö.O%Ã…ø^Ãø¥E$àù¤%M@r²" *Ò"Ò%YúÑM`/3†’ïPä(Á…õŒÀ ]/xþ"à.4À ð€R)M€$_Ý$(M 0ÀêRM ˆà,w«Å õ0…À5ü$(ï²ÒrMÖ ¾®*ÕDr’0MÀì òWZ£øsQ äò0Ë’*À(5óJ Àô¤1]6<ó*­$‹ÀCå8@3M‚r& &Õ²,a3ÜärÓRR= @6]å+"Tþ20É"M !2û!à};‰Ð€X0e!†Dà\‡¨(Öã‘ðÀTœˆ„NŒWD˜-‘^†ìý¸S`b °ŸlÜ+p@C>á„3®… X²ö¡ZÚF„ƒB@‚†B„gלù„æ–ö=T‚Ê aaIùÝe'fÀwÐùhÄ  ‘ÁuFhðš4À¬Ap)pœB0wA2'Ä|!µ€ Hð€cjv(å<S¢f8ãp€@¥‰…à)¼ÆàáhD^ŠñÚë;¯¾ºêdØm,`¥ íJ«Ž)ªi„þ~º+qD@©¥˜ªêkä`®¶¨ª¦\pã…ë+±åæéÐ(ÆláEh*ÝÃG‡„4º·G ÈÚŸ¡6§Y žÍhá¨DP8ßYié@Cà'€•.Ãxª`'g¸a‡@,¯A0Ä}„uHë3výû¯ÿ:&-P/Ç ×v@tK»Œ² À@È™ A²–€çÇó׎mse›pv˜‡ᵟÝÜP 8fWØ"XáqölbÞÒFt±p|„k\aév‡ÁÜŒ¹¦fË‚­œØ·‹Ðx‡ Ž+_ Q*þP3‚ûê,€·ÍášÑLÏ)¼]LÝáÌD ãCˆ†v‡µ›0€Bt>Äç¦ ¡iæCln{ôÓWÿ˜çϧ,íRï·BgG B @³á¬Ž¬â¯ÚŸ8äFDU‚ÿI ø4Ï€À(Þž—³î',ƒ`߈ ©ù GnÙ«ìt5-·™ ÈŽÀ;$dÇN iŽ#½ôGw,]˜2Ð̃BðŒh³—æ=)ž‰¡fÀŠ:œ!ƒ ”A qRZŸBˆöD»å¥‡F $f-‹JàÀ€:#pkxsºÊzÊ(ÐmPtÙ‹ PþŠ`! ž/U!‹ôFwÇ !)H¸Œþ’§8 ï<,B^Œ7œ*j'k,‚gz¸–Ë[eM°Ár“9¼‹ 0Y„66”&H¢Æî VEð„@(ȲµTB À€26¾èF› *qôé`8´ª¡òuÆ42Ó&? BŽÃa {W„D^3Xä5m)ÆSz6¤¦ ,9ÃphgLf¾“Äaá‡ÄAeWy×DÓ3ÜÓÛóìf Ô@æ$G‚ ô2 ÛK‚õB6£Ç É ÎQ†“PB®Ž0‚±èù0z³ïŒsvâ ’ˆþ4Â$x““I:Âp7„Î!RÐë =E9ѹá38EªhÉÓñ]¯|¸ãZTÉO‚„M¬ [Pó>" -[À»je˜Œi&4BÖ&$³'kmf ¨š :;3_ÚÐâL€À@¶ûŽiÄ22è<°›Ì¼fôˆ”ÕåÀy\*J$E΄ßé|šV³YªkùL"ÝÔúÓ!ŒÕî<ÏRE¹Y³]†­ã¬dc›JK €RбØ5cZšÉÄÆ3u«­Ø"€Û†yB7à˜Ø@€qù"¾ô5„ l Ê_óšÌUýª^±©qœ!èJžù8˜þ×ìv6µ¹ÍjÿH[öjôÚO¬\ƒ.nY•ì4Á¹8•/M:…¸»=nrmwÇü"¦¾Ì ¥ 6žÑ²– xÀ[Ѐ"Ë“¨‘)Á…ÍŠÖWvD x*0q…þ!Z‹ Ð,6} ›&ÀÀ„#P”¢E£¤Q–6¬ÎµèBÓ)Oº3$´å-k›K]Ö;[ãÙ ¹N (b=L%×evµ6–ÊòÙ–©Ä…ÆêÌo޽ä&-ѹv©S>Õüà+4ø€ÀM»`ðt+’o¡” }E¢Éð¡:¿¡Q{ ¬£`áQ†%8Ö ¾UþèÀÒÑpX@K­:j ï#€ôHð€Zå%¬ ~tyÍ*†­Ï!9ôF=-¹8Öm W¥›€Y2çÓÀN6LƒS"äí¨ÈV¶´Uò¸$P˜·.BC?‚n{ûÛà·¸ÇMîr›ûÜèN·º×Íîv»ûÝðŽ·¼çMïzÛûÞøÎ·¾÷Íï~Çûð-4œ«G\–º½DC£ÂÎð†;üá¸Ä'NñŠ[üâϸÆ7ÎñŽ{üã ¹ÈGNò’›|xˆ‹†s£¹RJ›Íã-s6àïšÛüæâFÎwÎó~ë¼ç@ú¼.ô¢ÝD#44€Æn€Ûâ#M þ¼þ4Févu96`] ®SÃëÒÀϰ¡q4F}:ÒpŸÅž °KÃíÑ€û3غ_CîÏÀ»3ôÎ »»Ãï]×ß™1xež‡ŸFÊ«±xj4>ìZGâ§MùAL>—¯¼æûùst~ó ¿ÃçË1úЛ>¥GêOÏú5¬>¯o½ìËûoÔ~ö¸ÿÂí»±ûÜû> ½ßFðOü) ?Ç/¾ò›ük4ùÐ?Âó«1ýè[?ëcG|ä¯Ïý+Tßï~ñà ò‹ß÷æŸûöÏÏ~'¤ßïo?ëãß÷õËÿþÒ·ÿ:èÿÍóßðú×ý÷É@€(mx x€À¶þ€Åà€ èh8 ¬UÁ€¸Jø ¸~ó½ ‚ h$¸ 'X‚O‘‚¹À‚*h.x 1ø‚31ƒµ`ƒ488 ;˜ƒ-у±„>¨Bø E8„'q„­ „HHL¸ OØ„!…©@…RèVx Yx…±…¥à…\ˆ`8 c†Q†¡€†f(jø m¸†ñ† ‡pÈt¸ wX‡ ‘‡™À‡zh~x ø‡1ˆ•`ˆ„ˆ8 ‹˜ˆýЈ‘‰Ž¨’ø•8‰÷p‰ ‰˜Hœ¸ŸØ‰ñЉ@Š¢øwˆy©xŠÊfŠ‡àŠ¬¨°X³‹žþ·Š·˜}¶ØŠ¸hµ¸‹äð‹‚ ŒÀ{½HzÇXŒüDŒ€ÀŒÊÈ{ɨzÑøŒ!8ƨ‹Ôø`ÎèÛ˜ØÐ|ŽÞH}Öâ8ŽàW޶§ŽèƒìØØŽësŽy@òïÈ öxõè°ü€þ˜‹ÙTYYÆsÀ Iù(|ùD8‘Èg‘™„é|™‘NØ‘ä8Yƒ I 9’#X’é(’(©ƒ* y,Ù’.q’p@“2‰ 6é9y“7ø’åç“¢ÅøÜÐ `à3p fùÐÍо°`]€~\Ñ!M–¤¶Ñiv±§†˜1o‹¶Ò–×ÒKpX% p7Ìq ÓaªÓèqàÓf9°ÔLÝÔNýÔ@D½·™´á.þZ6¿@ÐÕ^ýÕ`ÖPSM·4«v)€¼ÌÕbýÖaMÖA-ÔkZÖªauŽ•n ×|=Öv­¶;)0zÝ×}-×tͼsýÂëî‚k~¹Õ†Í×ˆØØ»Ø|àõbÔ7­{’=Ùo]Ù–MœÝM¾{ Ú`-Ú£íÁ˜½¼ÏŸÍÚ­]Úd ¯«mÛ]íÚ¯=Œ¸Mø±Û¼íÛ¿ÝŒÁ¤ÄmÛÆ}ÜܘÜr0ܼ}Û±ýÚº=Ý_ÝÜÎŽÐzµÝڽ݄ÜÝp»Ü¬Þâ×ݽMÞQ»ÞìÞX?ôÖÙt ßà ”øÔé-ÇjÞ -þßÞ·ÕP]àP-ÕÕ ´ø=ݾ©PìÝÞ Î³ ^Üúýàî×~³ÎÜáÞàBÝáçýá.â8Mânâ!îÞªâ“âÆ‡á-¾á2 ã†-ãR€ ß.nŸ8~Ø,îã6î²ANÙCžßEž²G×:ξ4Näý}ɽ à1žä þãîÙä¡å¾ä"ËåbýäPÀã'®åá)æqíåæ«æÔ=§=®äSçÙÍæ%îækç^Mæ<çY®çËç.çg.èKènè5^çhî”ßèŒ.åý­è~î~QNç•þèé_>éšžÞ–Žç+Žè ;êíjæþ¾é¦î Ò½ê‘ èŸÎêÿ}è .é´ž§Vžã¤~å­.°¨~ë³.êœ^ذîÙ²ÞæŽþë|ééÊžÁÉžçË^ëÇNÓÑ^êÓ®ë¶íÛNìÌŽ‡Î.íÜ^íÎìãNéÞNíèŽìÝ.Þæëí¾ÝïÎîä~Üóníñ^îÅ®Úù.™™Žëé®íõ~ ÿ>ìî¾ïZ½ëBžêÿì_åýNð×îëÙñÏ™ /îŠ ä ?ñ¼þíözï…šñØžë¿îø~ñ£Mòþò _ññó/óþÚñNÞë!óÚ§ó]þñ4ßòo–@?æ<ó(?ó,¼&OñKŸóCïôþ6¿ó"/¯./ñSŸØY_ó[ßEï¦G¿æBßô\ö7<öq~î¡~ðW°á~òðnö`ÿö«öw^ö*¯ïvo ¯¾÷/ÿõU@ß6Ûh?§xßçIóÌßaÛõÀ÷ô=ßÖ^ùLà†ß÷‹™ø…ÎöO´Oõ‚OªúC;ú^õA/쿵ªïà¬ôzßöXûUpú¶_ò³Oö™¯î»øtŸû’¯ô^‹û¦_üŒ?÷¨¯à‡ßÖ¥?㽿öÌü¢ÿü‘Íù‹îù¿òÍOáØïÙÚéH:ýyïúûáOÓã¿øVþr¯þš²í_ûŸïýÖŸúë_òõþÿP_ý@`‰Åa2-™Mç•N©Uë›Õn¹]ïÉeó™ J¢Ùm÷»ª¦éu RÙVWìýãZ€Ï¯î1Qq‘±ÑñRK-/²ÒMްmOÓÎ0pðÓ(ôò5Uu•µqÒ5V6“´”S°Ö–wTWÈTVx˜¸Øø8 yùªb À€#jÀúšÄ‰öøöÌ“»›÷;7üΛ9]}½ÝMÙ}A ãã`J@äD mæ˜ FÎׯƒfÀ…[bD‰×Á£h¬>& èC×d·‡dŠüH²œÃ“Y¶tùR‘E˜®hRíÓþjãTɳLI…+ÅÕ5rfR¥K™N‘ÙôÔMBÈðdß„xr(P” M–Ò¤X¨kÙ¶øÔí£ šŒ€á‰ >d€æ$AsçÔEöÝá²…ã6vü˜\ȉ ÐebïôŒÔ0ÙZ ÏŠ&EzrjÕ«ßHfÝÁT&!\¥ÂÀf`Ÿi›þ„ú‹ÑÑf_7~‹käd¸I÷@ ¨'Ø­ ¸ᧉW<œñrñãWo`pž‚îä¹dœ¶dAG*›;ëþ¼¾Ëößø¹èÇΟ½\K¹¹À­žæò1ážàÀ0øË3ƒ”ä;þîÂóηÿ64Ä_*PÄf  š÷ÀA"0 €p@„¯³ðÃü2Ü/Çþvô°Ä …„‰Ä!ák±Þîëq ÿÉÎÈ(¥¦È)q¹.IQ–TG+½ü’˜*ÁIð¸üéLÞÆ\“ÍKÄlS»ÑÔ²K:ç„Ï<ySÏ,ÊÔ0MÄìT³ÏB 5ŒÉC©ø“Ç@³ìäÇ'/T”ÒCù¬4 FTPH;”4QLEÅóÒQí«ÓÓ-9}ÔÔV+-ÕUMAuÔÌUkuWK'UNB{Q5UTs6OX[•Õ(}ü4Ù]¯p²ÙP‰6DcMEviŸ4ZZ¥Ü1­þ[PœµÚl½m4Üv­WÔrëP¶InÕµõ[wõ^L奃^ ™½7Ø;÷=¸ÚsÉíµÓ_…EX„%°ßWf⇲×\m'þØ­Š)ýw—u7-ØWU6NdEI."`?9žWáEg¸æ•u&2g‹±¼Õaƒ3zç¢kYןó :åîØè¨!CÚЗ‰ˆ9¹›KÆ—]©½n‹êB­þÃã8´†¹çLϾ:í¯Ý^&ì>ÇÇäY¹>ùí¼gŠ[Ϲ «»Û»íÖ›p—ø.öb ‡n:¨µÉ.r–'5ñ¥oørŒ#ß¼ÉáôëmŸ¦¹l›GǹtÎU?Åó6AoþÛÆÓ·FóÕm?¦u6_O]Št¡<ðÛ…7&÷5wœ`¦kžùVŠ·ò®•×¼qÙÑæ½ùìÁÞåèñÎ\q§#Öž|Öa¯ÚûÁ§¿úñË’çÁ<øäÁ·þü‘ÿKúi§~,îÓß÷t>±¥/xëÃ_û4F@nOeþS ôîGÁ^ÜKšØÀ ƒ!DTh?ñyLŽ£›Y˜B ¢Oid ?ȸÞPf/<` ¿wB¦ÐzlÓ!È?/I°‚=œ¡ ‰ØÄÞPn%üÝÿØWÃå9‹6bߤH:äMq‚JÌâ—`Äwuu_ôbýÀHF7šþÀŒSB¢™¾Ñ‰q”Ò—xE ÞDÔc”øhEþ…Äá THDþÐŽ‰Td É/4Î.ŒêK¢&'ÙÄJ É‘‘„diÙI~2H¡$¥$Áà»5š2Pä"9ÙÇCŠ²Ž°Ä *K¤ÊVªðol|¥.)KÄÑ2›L&ýHÌ òRD¾äP)ÄÇ9³˜[<&5§ÉÊnŽ›E4&åiB[Vñ‘¹ §þ ™°r¶‘™·\%8× ¿v‚Hšq²æ ©¸ÀzðžʧŽöÌ~ÊðŸâÔ&9¹©Ï†ô¡ m^@4ÐeE´^ DUGQŠ]òzjL£0GÊQz4þ@Í(FÆR“®¥ìQiK½éК¾Tx1%ÏLe¦Qcº²¤8ÍŸNÇÃÓ¬ùT›@ŤPßGTñUt.=ªT™Z8§.ªèæF͆ԪuœŸ©EºÔx¢ó«9 «ëÆz̓Š1ÍL«í®Šœ¬v•ªZõê\ÉW×ãÜÕtyÅëMùjյꮭüÌä2ÏéÏÂÂô°ÆK¬AkNC¢õ±›ó+Ë&ËÕÀv¥ ÕB RZÓžµ¨HfÙºP±¾s˜Ê´l\åùËkÜ·¹Õ­kY›šÍ°NÙêO‡›T‚T¹ÉUîr™[ D¶·Žùík‚ûĽ¾µ–—ult Ýùu–¸þ×­,<‹Pîv—·ˆ…mPeK^íš—r=o/½Û?ðW¼ííY·;_ú¦W²ë5kyáßÚ~SþÅg}xß²†”¤~oœ0COÀÆ.c'œ]Úb¶Âeð3LÖkx¼û%°‡C¼à ·ÄnM1{ù ßy&¸Å2±|box¶qŽuübûÆX±ú¥ñŠ9üáþ¹È$D2e•,a'Û¸š‚…²ow¼ÇÙ½W¦p–E»eêv™_ž±•…üdË×ÌXEsw§ìYáæ·Æc~óãü×9[²ÎáÕòmÝB÷9d¥š«ŒâFÿxÍŽFt”AV]µá™ÉAþ¾1œ'}f#78Ðø=4”*éL‡¹Ó~þ4‰G»VÓnrª9»j‡úÁ~´ŒwdZËÙÖ^ƵygošÏ¿žÌtYciR7×Å>µŠ• \E§’ÑÆž5²‡lSzV{jצï°ÁLm1›Ìß·tÅÍlO{ɲÖtºq¼îÆ0{5Îvu™§Êï¨úÛÞLÁ·jô ëW&ÖçÞvÀÙlÈ8Ò jxàÙ䎴®µ=ï=w›á`k7[æ‘ <¡À<À€`¼Bê¾(À+óÐÚ<äMx<ÜÄø@!€H9l¤ñ^S¹ã`\¼Q “¥Sêéºrþ¸cšÓ 'NX€š€żÞ3ÇyO#f¦›{&f§”Ú¥n¨}6VyWš˜§áÝg·Â“Çvn»¹%~7”à£8õÆÌ¥.wyB Є@EeÌ;ß›Nï†ÞÛa§æõÄyÊ>.•I|f˜PÆ;ò`ç4DiþYƒyþ暟ìáD{Ä‚Þ-R¡Jmž0€ú,ÁîNx¼(P|ãùɧÀðaZlúÑ—þô©}˜¶Pþö•Ïü 8¿úáÿõK›}îŸßøÞ7-Äßþé“ æGÿùÕ_Úç»ÿðÈýñßþœ–ÿú/üô@Ûðþ§ïÿÀo«/P©Ù¯¥/3ÐúN«8°7p±ë œƒ ®.뚀¸Ž ¼®E˜ tËosPw{ЃP‡‹Ð “P — ›Ð Ÿ £0£Oz>äà ‚.ë†î H`þ!µÂP Ç ËÐ Ï ÓP × ÛÐ ßãPçëÐïóP÷Së6  ¥ä<à¤9Z$å €å\®sÎw¦"E@Eàë–€E.î;Ñ?CQÁ-#.òòäñp ÷–"ç$@ Fþ FÀ¤à›Ào ¿ÄF€xd‘3j±]JÎNNO<à.ÀðV‹MD@à.  6ãâ8@åÑK¨Ñ™ÀÀ0¾¤ 8à^n ªb»±]¬Ð~®Oœñõ„ı @Vk ؤÇq ÊO¼ÑJ 2 —`3 2\ªnOPOoóä Kïô|®Á¤#ÉQ eëò#á#$§E÷fƒ÷šq+bdNFÒDà%— @¿Ä&M qq €!×Äô:'mC'y2\D ú‘ôH%øB î®&ýq 4²ôþdpM~20Àô"zrLÀM€+cð©Å`ô(…@6R+Õ2õØr}ò.Ÿàê¢L$à%×r ¼®(‡%6vO)À,¿ò.“² vò1§ä'ÏØD0I6“2+²$M€+¥9*S$ïÒô$ ²/1sM!— %ïÑLÓUì‘#*e.ë2+c ’ Y36àêÓJ¼à7%2’3\pC1/õÄ$€úÂ*±rM:€ö¡8€B; èL¸Ó;Á³F€= v²6ÁÄë†ñ> ëÆ³<à0‰ÅPDâ Þ|o™¢þn+.1Œ‘×ä@tH  A @lsH@·Ò€A‘q?DCTDG”DKÔDOESTEW”E[ÔE_FcTFg”FkT ¸1A\'© . lTHY¡àF @€´ösñ`HŸ3 0Ô ¨å² 2J·Êñ@0™@;à àé>à@2 fp F†‘$ÔâL MeÄ9q.€B;'5` 3Ðr q .¹”QA‚@ .`G— ÀL= 0²*¤¡8â½î¸N£ @,1n < 1Uþ'`"DìbA(Õ.@BÀIá(†QRUXAµRMQz-½Ñ*d U³!@K-ñéÔ#H— 9j'C²G•ôR xuXËŠÕ(• î–,ƒî˜@0$è¢!27,±'áõò')R"OŽR‘1è‘\ÍaÑàà=ÂÔRÕõ`— èÄë6(C“6³‘÷ø”cÕÓ÷¡E&6aOV ÐU]—@›àNÔŽRéòPK[5–_™Àó38öV`d/ÑdQ–h»]Y!vQ›u R®MoRñ @!]²uE 5jÙd)Õ«¡EâµhÃv ö=VÖ ^6SäS´E¼"„"`-Òç€U]`5kMÀV5h[àÿÒQlw ŽöXÑÖ]U-áT"À뤱ŒÑ÷`ÎnÝOÓtOûö[Óh™€dãNÀquÙà$¹`uS×ueXŽ6d·àN_w]!(4 Ð Z7wƒWx‡—x‹×xy“Wy——y›×yŸz£Wz§—z«×z¯{³W{·—{»×{¿|ÃW|Ç—|Ë×|!";andrewpeterson-amp-4878fc892f2c/docs/_static/branches.svg000066400000000000000000000440311332417112400233340ustar00rootroot00000000000000 image/svg+xml 0.4 0.5 0.4.1 master v0.4 v0.5 andrewpeterson-amp-4878fc892f2c/docs/_static/completeexample.py000066400000000000000000000042771332417112400245740ustar00rootroot00000000000000from ase.lattice.surface import fcc110 from ase import Atom, Atoms from ase.constraints import FixAtoms from ase.calculators.emt import EMT from ase.md import VelocityVerlet from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ase import units from ase import io from amp.utilities import randomize_images from amp import Amp from amp.descriptor import * from amp.regression import * ############################################################################### def test(): # Generate atomic system to create test data. atoms = fcc110('Cu', (2, 2, 2), vacuum=7.) adsorbate = Atoms([Atom('H', atoms[7].position + (0., 0., 2.)), Atom('H', atoms[7].position + (0., 0., 5.))]) atoms.extend(adsorbate) atoms.set_constraint(FixAtoms(indices=[0, 2])) calc = EMT() # cheap calculator atoms.set_calculator(calc) # Run some molecular dynamics to generate data. trajectory = io.Trajectory('data.traj', 'w', atoms=atoms) MaxwellBoltzmannDistribution(atoms, temp=300. * units.kB) dynamics = VelocityVerlet(atoms, dt=1. * units.fs) dynamics.attach(trajectory) for step in range(50): dynamics.run(5) trajectory.close() # Train the calculator. train_images, test_images = randomize_images('data.traj') calc = Amp(descriptor=Behler(), regression=NeuralNetwork()) calc.train(train_images, energy_goal=0.001, force_goal=None) # Plot and test the predictions. import matplotlib matplotlib.use('Agg') from matplotlib import pyplot fig, ax = pyplot.subplots() for image in train_images: actual_energy = image.get_potential_energy() predicted_energy = calc.get_potential_energy(image) ax.plot(actual_energy, predicted_energy, 'b.') for image in test_images: actual_energy = image.get_potential_energy() predicted_energy = calc.get_potential_energy(image) ax.plot(actual_energy, predicted_energy, 'r.') ax.set_xlabel('Actual energy, eV') ax.set_ylabel('Amp energy, eV') fig.savefig('parityplot.png') ############################################################################### if __name__ == '__main__': test()andrewpeterson-amp-4878fc892f2c/docs/_static/convergence.svg000066400000000000000000016360261332417112400240610ustar00rootroot00000000000000 andrewpeterson-amp-4878fc892f2c/docs/_static/fpranges.svg000066400000000000000000010225571332417112400233660ustar00rootroot00000000000000 andrewpeterson-amp-4878fc892f2c/docs/_static/gaussian.svg000066400000000000000000057763031332417112400234040ustar00rootroot00000000000000 image/svg+xml0.5 1.0 1.5 2.0 2.5 3.0 0.2 0.4 0.6 0.8 1.0 0.2 0.4 0.6 0.8 1.0 0.2 0.4 0.6 0.8 1.0 andrewpeterson-amp-4878fc892f2c/docs/_static/nn.svg000077500000000000000000001256611332417112400221760ustar00rootroot00000000000000 image/svg+xml input layer hidden layer output layer input # 1 input # 2 input # 3 input # 4 output(energy) biases 11 (1) 31 (2) 41 (2) andrewpeterson-amp-4878fc892f2c/docs/_static/nodeplot-Pt.svg000066400000000000000000006531741332417112400237720ustar00rootroot00000000000000 andrewpeterson-amp-4878fc892f2c/docs/_static/parity_error_sensitivity.svg000066400000000000000000060245261332417112400267570ustar00rootroot00000000000000 image/svg+xml 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 ab initio energy, eV 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 Amp energy, eV Energies 20 15 10 5 0 5 10 15 20 ab initio force, eV/Ang 20 15 10 5 0 5 10 15 20 Amp force, eV/Ang Forces 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 ab initio energy (eV) per atom 0.0000 0.0005 0.0010 0.0015 0.0020 ab initio energy - Amp energy / number of atoms energy rmse = 0.00051 Energies 20 15 10 5 0 5 10 15 20 ab initio force, eV/Ang 0.000 0.005 0.010 0.015 0.020 0.025 0.030 0.035 0.040 ab initio force - Amp force force rmse = 0.0080 Forces 0.00025 0.00020 0.00015 0.00010 0.00005 parameter no 66 2.186e1 1 2 3 4 loss function 1e8+1.4089e4 andrewpeterson-amp-4878fc892f2c/docs/_static/zernike.svg000066400000000000000000132513721332417112400232320ustar00rootroot00000000000000 image/svg+xml0.2 0.4 0.6 0.8 1.0 0.5 1.0 1.5 2.0 2.5 0.5 1.0 1.5 2.0 2.5 3.0 1 2 3 4 0.5 1.0 1.5 2.0 2.5 3.0 0.5 1.0 1.5 2.0 andrewpeterson-amp-4878fc892f2c/docs/analysis.rst000066400000000000000000000022531332417112400217550ustar00rootroot00000000000000.. _Analysis: ================================== Analysis ================================== ---------------------------------- Convergence plots ---------------------------------- You can use the tool called `amp-plotconvergence` to help you examine the output of an Amp log file. Run `amp-plotconvergence -h` for help at the command line. You can also access this tool as :func:`~amp.analysis.plot_convergence` from the :mod:`amp.analysis` module. .. image:: _static/convergence.svg :width: 600 px :align: center ---------------------------------- Other plots ---------------------------------- There are several other plotting tools within the :mod:`amp.analysis` module, including :func:`~amp.analysis.plot_parity` for making parity plots, :func:`~amp.analysis.plot_error` for making error plots, and :func:`~amp.analysis.plot_sensitivity` for examining the sensitivity of the model output to the model parameters. These modules should produce plots like below; in the order parity, error, and sensitivity from left to right. See the module autodocumentation for details. .. image:: _static/parity_error_sensitivity.svg :width: 1000 px :align: center andrewpeterson-amp-4878fc892f2c/docs/building.rst000066400000000000000000000311101332417112400217210ustar00rootroot00000000000000.. _Building: ================================== Building modules ================================== Amp is designed to be modular, so if you think you have a great descriptor scheme or machine-learning model, you can try it out. This page describes how to add your own modules; starting with the bare-bones requirements to make it work, and building up with how to construct it so it integrates with respect to parallelization, etc. ---------------------------------- Descriptor: minimal requirements ---------------------------------- To build your own descriptor, it needs to have certain minimum requirements met, in order to play with *Amp*. The below code illustrates these minimum requirements:: from ase.calculators.calculator import Parameters class MyDescriptor(object): def __init__(self, parameter1, parameter2): self.parameters = Parameters({'mode': 'atom-centered',}) self.parameters.parameter1 = parameter1 self.parameters.parameter2 = parameter2 def tostring(self): return self.parameters.tostring() def calculate_fingerprints(self, images, cores, log): # Do the calculations... self.fingerprints = fingerprints # A dictionary. The specific requirements, illustrated above, are: * Has a parameters attribute (of type `ase.calculators.calculator.Parameters`), which holds the minimum information needed to re-build your module. That is, if your descriptor has user-settable parameters such as a cutoff radius, etc., they should be stored in this dictionary. Additionally, it must have the keyword "mode"; which must be set to either "atom-centered" or "image-centered". (This keyword will be used by the model class.) * Has a "tostring" method, which converts the minimum parameters into a dictionary that can be re-constructed using `eval`. If you used the ASE `Parameters` class above, this class is simple:: def tostring(): return self.parameters.tostring() * Has a "calculate_fingerprints" method. The images argument is a dictionary of training images, with keys that are unique hashes of each image in the set produced with `amp.utilities.hash_images`. The log is a `amp.utilities.Logger` instance, that the method can optionally use as `log('Message.')`. The cores keyword describes parallelization, and can safely be ignored if serial operation is desired. This method must save a sub-attribute `self.fingerprints` (which will be accessible in the main *Amp* instance as `calc.descriptor.fingerprints`) that contains a dictionary-like object of the fingerprints, indexed by the same keys that were in the images dictionary. Ideally, `descriptor.fingerprints` is an instance of `amp.utilities.Data`, but probably any mapping (dictionary-like) object will do. A fingerprint is a vector. In **image-centered** mode, there is one fingerprint for each image. This will generally be just the Cartesian positions of all the atoms in the system, but transformations are possible. For example this could be accessed by the images key >>> calc.descriptor.fingerprints[key] >>> [3.223, 8.234, 0.0322, 8.33] In **atom-centered** mode, there is a fingerprint for each atom in the image. Therefore, calling `calc.descriptor.fingerprints[key]` returns a list of fingerprints, in the same order as the atom ordering in the original ASE atoms object. So to access an individual atom's fingerprints one could do >>> calc.descriptor.fingerprints[key][index] >>> ('Cu', [8.832, 9.22, 7.118, 0.312]) That is, the first item is the element of the atom, and the second is a 1-dimensional array which is that atom's fingerprint. Thus, `calc.descriptor.fingerprints[hash]` gives a list of fingerprints, in the same order the atoms appear in the image they were fingerprinted from. If you want to train your model to forces also (besides energies), your "calculate_fingerprints" method needs to calculate derivatives of the fingerprints with respect to coordinates as well. This is because forces (as the minus of coordinate-gradient of the potential energy) can be written, according to the chain rule of calculus, as the derivative of your model output (which represents energy here) with respect to model inputs (which is fingerprints) times the derivative of fingerprints with respect to spatial coordinates. These derivatives are calculated for each image for each possible pair of atoms (within the cutoff distance in the **atom-centered** mode). They can be calculated either analytically or simply numerically with finite-difference method. If a piece of code is written to calculate coordinate-derivatives of fingerprints, then the "calculate_fingerprints" method can save it as a sub-attribute `self.fingerprintprimes` (which will be accessible in the main *Amp* instance as `calc.descriptor.fingerprintprimes`) along with `self.fingerprints`. `self.fingerprintprimes` is a dictionary-like object, indexed by the same keys that were in the images dictionary. Ideally, `descriptor.fingerprintprimes` is an instance of `amp.utilities.Data`, but probably any mapping (dictionary-like) object will do. Calling `calc.descriptor.fingerprintprimes[key]` returns the derivatives of fingerprints for the image key of interest. This is a dictionary where each key is a tuple representing the indices of the derivative, and each value is a list of finperprintprimes. (This list has the same length as the fingerprints.) For example, to retrieve derivatives of the fingerprints of atom indexed 2 (which is say Pt) with respect to :math:`x` coordinate of atom indexed 1 (which is say Cu), we should do >>> calc.descriptor.fingerprintprimes[key][(1, 'Cu', 2, 'Pt', 0)] >>> [-1.202, 0.130, 4.511, -0.721] Or to retrieve derivatives of the fingerprints of atom indexed 1 with respect to :math:`z` coordinate of atom indexed 1, we do >>> calc.descriptor.fingerprintprimes[key][(1, 'Cu', 1, 'Cu', 2)] >>> [3.48, -1.343, -2.561, -8.412] ---------------------------------- Descriptor: standard practices ---------------------------------- The below describes standard practices we use in building modules. It is not necessary to use these, but it should make your life easier to follow standard practices. And, if your code is ultimately destined to be part of an Amp release, you should plan to make it follow these practices unless there is a compelling reason not to. We have an example of a minimal descriptor in `amp.descriptor.example`; it's probably easiest to copy this file and modify it to become your new descriptor. For a complete example of a working descriptor, see `amp.descriptor.gaussian`. The Data class ^^^^^^^^^^^^^^^^^^^ The key element we use to make our lives easier is the `Data` class. It should be noted that, in the development version, this is still a work in progress. The `Data` class acts like a dictionary in that items can be accessed by key, but also saves the data to disk (it is persistent), enables calculation of missing items, and can even parallelize these calculations across cores and nodes. It is recommended to first construct a pure python version that fits with the `Data` scheme for 1 core, then expanding it to work with multiple cores via the following procedure. See the Gaussian descriptor for an example of implementation. Basic data addition """"""""""""""""""" To make the descriptor work with the `Data` class, the `Data` class needs a keyword `calculator`. The simplest example of this is our `NeighborlistCalculator`, which is basically a wrapper around ASE's Neighborlist class:: class NeighborlistCalculator: """For integration with .utilities.Data For each image fed to calculate, a list of neighbors with offset distances is returned. """ def __init__(self, cutoff): self.globals = Parameters({'cutoff': cutoff}) self.keyed = Parameters() self.parallel_command = 'calculate_neighborlists' def calculate(self, image, key): cutoff = self.globals.cutoff n = NeighborList(cutoffs=[cutoff / 2.] * len(image), self_interaction=False, bothways=True, skin=0.) n.update(image) return [n.get_neighbors(index) for index in range(len(image))] Notice there are two categories of parameters saved in the init statement: `globals` and `keyed`. The first are parameters that apply to every image; here the cutoff radius is the same regardless of the image. The second category contains data that is specific to each image, in a dictionary format keyed by the image hash. In this example, there are no keyed parameters, but in the case of the fingerprint calculator, the dictionary of neighborlists is an example of a `keyed` parameter. The class must have a function called `calculate`, which when fed an image and its key, returns the desired value: in this case a neighborlist. Structuring your code as above is enough to make it play well with the `Data` container in serial mode. (Actually, you don't even need to worry about dividing the parameters into globals and keyed in serial mode.) Finally, there is a `parallel_command` attribute which can be any string which describes what this function does, which will be used later. Parallelization """"""""""""""" The parallelization should work provided the scheme is `embarassingly parallel `_; that is, each image's fingerprint is independent of all other images' fingerprints. We implement this in building the `amp.utilities.Data` dictionaries, using a scheme of establishing SSH sessions (with pxssh) for each worker and passing messages with ZMQ. The `Data` class itself serves as the master, and the workers are instances of the specific module; that is, for the Gaussian scheme the workers are started with `python -m amp.descriptor.gaussian id hostname:port` where id is a unique identifier number assigned to each worker, and hostname:port is the socket at which the workers should open the connection to the mater (e.g., "node243:51247"). The master expects the worker to print two messages to the screen: "" which confirms the connection is established, and ""; the text that is between them alerts the master (and the user's log file) where the worker will write its standard error to. All messages after this are passed via ZMQ. I.e., the bottom of the module should contain something like:: if __name__ == "__main__": import sys import tempfile hostsocket = sys.argv[-1] proc_id = sys.argv[-2] print('') sys.stderr = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.stderr') print('stderr written to %s' % sys.stderr.name) After this, the worker communicates with the master in request (from the worker) / reply (from the master) mode, via ZMQ. (It's worth checking out the `ZMQ Guide `_; (ZMQ Guide examples). Each request from the worker needs to take the form of a dictionary with three entries: "id", "subject", and (optionally) "data". These are easily created with the `amp.utilities.MessageDictionary` class. The first thing the worker needs to do is establish the connection to the master and ask its purpose:: import zmq from ..utilities import MessageDictionary msg = MessageDictionary(proc_id) # Establish client session via zmq; find purpose. context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect('tcp://%s' % hostsocket) socket.send_pyobj(msg('')) purpose = socket.recv_pyobj() In the final line above, the master has sent a string with the `parallel_command` attribute mentioned above. You can have some if/elif statements to choose what to do next, but for the calculate_neighborlist example, the worker routine is as simple as requesting the variables, performing the calculations, and sending back the results, which happens in these few lines. This is all that is needed for parallelization (in pure python):: # Request variables. socket.send_pyobj(msg('', 'cutoff')) cutoff = socket.recv_pyobj() socket.send_pyobj(msg('', 'images')) images = socket.recv_pyobj() # Perform the calculations. calc = NeighborlistCalculator(cutoff=cutoff) neighborlist = {} while len(images) > 0: key, image = images.popitem() # Reduce memory. neighborlist[key] = calc.calculate(image, key) # Send the results. socket.send_pyobj(msg('', neighborlist)) socket.recv_string() # Needed to complete REQ/REP. andrewpeterson-amp-4878fc892f2c/docs/community.rst000066400000000000000000000016101332417112400221520ustar00rootroot00000000000000.. _Community: ================================== Community ================================== ---------------------------------- Mailing list ---------------------------------- An amp-users listserv is available for general discussion, troubleshooting, suggestions, etc. It is available at https://listserv.brown.edu/?A0=AMP-USERS The archives of this list are also available to members of the list. ---------------------------------- Bugs and issues ---------------------------------- To report bugs, issues, works-in-progress, or feature requests (although those might best be first discussed on amp-users), please use our Issue Tracker on the repository page. It is available at https://bitbucket.org/andrewpeterson/amp/issues ---------------------------------- Contributions ---------------------------------- You are welcome to contribute to this project. See the :any:`Develop` page. andrewpeterson-amp-4878fc892f2c/docs/conf.py000066400000000000000000000251361332417112400207040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # AMP documentation build configuration file, created by # sphinx-quickstart on Thu Jul 30 17:27:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('../')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Amp' copyright = u'2015--current, Andrew A. Peterson, Alireza Khorshidi' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. release = '0.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'bizstyle' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = '_static/amp.png' html_logo = '_static/amp-logo.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' #html_last_updated_fmt = '%a, %d %b %Y %H:%M:%S' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Ampdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Amp.tex', u'Amp Documentation', u'Andrew A. Peterson, Alireza Khorshidi', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'amp', u'Amp Documentation', [u'Alireza Khorshidi, Andrew A. Peterson'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Amp', u'Amp Documentation', u'Andrew A. Peterson, Alireza Khorshidi', 'Amp', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'Amp' epub_author = u'Andrew A. Peterson, Alireza Khorshidi' epub_publisher = u'Andrew A. Peterson, Alireza Khorshidi' epub_copyright = u'2015--current, Andrew A. Peterson, Alireza Khorshidi' # The basename for the epub file. It defaults to the project name. #epub_basename = u'AMP' # The HTML theme for the epub output. Since the default themes are not optimized # for small screen space, using the same theme for HTML and epub output is # usually not wise. This defaults to 'epub', a theme designed to save visual # space. #epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. #epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True # Choose between 'default' and 'includehidden'. #epub_tocscope = 'default' # Fix unsupported image types using the PIL. #epub_fix_images = False # Scale large images. #epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. #epub_show_urls = 'inline' # If false, no index is generated. #epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} andrewpeterson-amp-4878fc892f2c/docs/credits.rst000066400000000000000000000027031332417112400215670ustar00rootroot00000000000000.. Amp documentation master file, created by sphinx-quickstart on Thu Jul 30 17:27:50 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Credits ======= People ------ This project is developed primarily by **Andrew Peterson** and **Alireza Khorshidi** in the Brown University School of Engineering. Specific credits: * Andrew Peterson: lead, PI, many modules * Alireza Khorshidi: many modules, Zernike descriptor * Zack Ulissi: tensorflow version of neural network * Muammar El Khatib: general contributions We are also indebted to Nongnuch Artrith (MIT) and Pedro Felzenszwalb (Brown) for inspiration and technical discussion. Citations --------- We would appreciate if you cite the below publication for any use of Amp or its methods: Khorshidi & Peterson, "Amp: A modular approach to machine learning in atomistic simulations", *Computer Physics Communications* 207:310-324, 2016. |amp_paper| .. |amp_paper| raw:: html [doi:10.1016/j.cpc.2016.05.010] If you use Amp for saddle-point searches or nudged elastic bands, please also cite: Peterson, "Acceleration of saddle-point searches with machine learning", *Journal of Chemical Physics*, 145:074106, 2016. |mlneb_paper| .. |mlneb_paper| raw:: html [DOI:10.1063/1.4960708] andrewpeterson-amp-4878fc892f2c/docs/databases.rst000066400000000000000000000057371332417112400220730ustar00rootroot00000000000000.. _Databases: ================================== Fingerprint databases ================================== Often, a user will want to train multiple calculators to a common set of images. This may be just in routine development of a trained calculator (e.g., trying different neural network sizes), in using multiple training instances trying to find a good initial guess of parameters, or in making a committee of calculators. In this case, it can be a waste of computational time to calculate the fingerprints (and more expensively, the fingerprint derivatives) more than once. To deal with this, Amp saves the fingerprints to a database, the location of which can be specified by the user. If you want multiple calculators to avoid re-fingerprinting the same images, just point them to the same database location. Format --------------------------------- The database format is custom for Amp, and is designed to be as simple as possible. Amp databases end in the extension `.ampdb`. In its simplest form, it is just a directory with one file per image; that is, you will see something like below:: label-fingerprints.ampdb/ loose/ f60b3324f6001d810afbab9f85a6ea5f aeaaa21e5faccc62bae94c5c48b04031 In the above, each file in the directory "loose" is the hash of an image, and contains that image's fingerprint. We use a file-based "database" to avoid conflicts with multiple processes accessing a database at the same time, which can cause conflicts. However, for large training sets this can lead to lots of loose files, which can eat up a lot of memory, and with the large number of files slow down indexing jobs (like backups and scans). Therefore, you can compress the database with the `amp-compress` tool, described below. Compress --------------------------------- To save disk space, you may periodically want to run the utility `amp-compress` (contained in the `tools` directory of the amp package; this should be on your path for normal installations). In this case, you would run `amp-compress `, which would result in the above `.ampdb` file being changed to:: label-fingerprints.ampdb/ archive.tar.gz loose/ That is, the two fingerprints that were in the "loose" directory are now in the file "archive.tar.gz". You can also use the `--recursive` (or `-r`) flag to compress all ampdb files in or below the specified directory. When Amp reads from the above database, it first looks in the "loose" directory for the fingerprint. If it is not there, it looks in "archive.tar.gz". If it is not there, it calculates the fingerprint and adds it to the "loose" directory. Future --------------------------------- We plan to make the amp-compress tool more automated. If the user does not supply a separate `dblabel` keyword, then we assume that their process is the only process using the database, and it is safe to compress the database at the end of their training job. This would automatically clean up the loose files at the end of the job. andrewpeterson-amp-4878fc892f2c/docs/develop.rst000066400000000000000000000101131332417112400215620ustar00rootroot00000000000000.. _Develop: ================================== Development ================================== This page contains standard practices for developing Amp, focusing on repositories and documentation. ---------------------------------- Repositories and branching ---------------------------------- The main Amp repository lives on bitbucket, `andrewpeterson/amp `_ . We employ a branching model where the `master` branch is the main development branch, containing day-to-day commits from the core developers and honoring merge requests from others. From time to time, we create a new branch that corresponds to a release. This release branch contains only the tagged release and any bug fixes. .. image:: _static/branches.svg :width: 400 px :align: center ---------------------------------- Contributing ---------------------------------- You are welcome to contribute new features, bug fixes, better documentation, etc. to Amp. If you would like to contribute, please create a private fork and a branch for your new commits. When it is ready, send us a merge request. We follow the same basic model as ASE; please see the ASE documentation for complete instructions. As good coding practice, make sure your code passes both the pyflakes and pep8 tests. (On linux, you should be able to run `pyflakes file.py` and `pep8 file.py`, and then correct it by `autopep8 --in-place file.py`.) If adding a new feature: consider adding a (very brief) test to the tests folder to ensure your new code continues to work, and also be sure to write clear documentation. Finally, to make users aware of your new feature or change, add a bullet point to the release notes page of the documentation under the Development version heading. It is also a good idea to send us an email if you are planning something complicated. ---------------------------------- Documentation ---------------------------------- This documentation is built with sphinx. (Mkdocs doesn't seem to support autodocumentation.) To build a local copy, cd into the docs directory and try a command such as .. code-block:: bash sphinx-build . /tmp/ampdocs firefox /tmp/ampdocs/index.html & # View the local copy. This uses the style "bizstyle"; if you find this is missing on your system, you can likely install it with .. code-block:: bash pip install --user sphinxjp.themes.bizstyle You should then be able to update the documentation rst files and see changes on your own machine. For line breaks, please use the style of containing each sentence on a new line. ---------------------------------- Releases ---------------------------------- To create a release, we go through the following steps. * Create a new branch on the bitbucket repository with the version name, as in `v0.5`. (Don't create a separate branch if this is a bugfix release, e.g., 0.5.1 --- just add those to the v0.5 branch.) All subsequent work is in the new branch. Note the branch name starts with "v", while the tag names will not, to avoid naming conflicts. * Change `docs/conf.py`'s version information to match the new version number. * Change the version that prints out in the Amp headers by changing the `_ampversion` variable in `amp/__init__.py`. * Change revision history to include this release; generally the changes should have been catalogued under a "Development version" heading. * Commit and push the changes to the new branch on bitbucket. * Tag the release with the release number, e.g., '0.5' or '0.5.1', the latter being for bug fixes. Do this on a local machine (on the correct branch) with `git tag -a 0.5`, followed by `git push origin --tags`. * Add the version to readthedocs' available versions; also set it as the default stable version. * Change the nightly tests to test this branch as the "stable" build. * Create a DOI for the release via zenodo.org. Note that all the ".git" files and folders should be removed from the files before uploading to Zenodo. The DOI can then be added to the development version's release notes. (I don't think there's a way to get it into the archival version on Zenodo!) andrewpeterson-amp-4878fc892f2c/docs/examplescripts.rst000066400000000000000000000145341332417112400232020ustar00rootroot00000000000000.. _ExampleScripts: ================================== Example scripts ================================== ---------------------------------- A basic fitting script ---------------------------------- The below script uses Gaussian descriptors with a neural network backend --- the Behler-Parrinello approach --- to train energies only to a training set made by the script. Note that most of the code is just generating the training data, and the training takes place in a couple of lines. .. code-block:: python """Simple test of the Amp calculator, using Gaussian descriptors and neural network model. Randomly generates data with the EMT potential in MD simulations.""" import os from ase import Atoms, Atom, units import ase.io from ase.calculators.emt import EMT from ase.lattice.surface import fcc110 from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ase.md import VelocityVerlet from ase.constraints import FixAtoms from amp import Amp from amp.descriptor.gaussian import Gaussian from amp.model.neuralnetwork import NeuralNetwork def generate_data(count, filename='training.traj'): """Generates test or training data with a simple MD simulation.""" if os.path.exists(filename): return traj = ase.io.Trajectory(filename, 'w') atoms = fcc110('Pt', (2, 2, 2), vacuum=7.) atoms.extend(Atoms([Atom('Cu', atoms[7].position + (0., 0., 2.5)), Atom('Cu', atoms[7].position + (0., 0., 5.))])) atoms.set_constraint(FixAtoms(indices=[0, 2])) atoms.set_calculator(EMT()) atoms.get_potential_energy() traj.write(atoms) MaxwellBoltzmannDistribution(atoms, 300. * units.kB) dyn = VelocityVerlet(atoms, dt=1. * units.fs) for step in range(count - 1): dyn.run(50) traj.write(atoms) generate_data(20) calc = Amp(descriptor=Gaussian(), model=NeuralNetwork(hiddenlayers=(10, 10, 10))) calc.train(images='training.traj') Note you can monitor the progress of the training by typing `amp-plotconvergence amp-log.txt`, which will create a file called `convergence.pdf`. ---------------------------------- A basic script with forces ---------------------------------- The below script trains both energy and forces to the same training set as above. Note this may take some time to run, which will depend upon the initial guess for the neural network parameters that is randomly generated. Try decreasing the `force_rmse` convergence parameter if you would like faster results. .. code-block:: python """Simple test of the Amp calculator, using Gaussian descriptors and neural network model. Randomly generates data with the EMT potential in MD simulations.""" import os from ase import Atoms, Atom, units import ase.io from ase.calculators.emt import EMT from ase.lattice.surface import fcc110 from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ase.md import VelocityVerlet from ase.constraints import FixAtoms from amp import Amp from amp.descriptor.gaussian import Gaussian from amp.model.neuralnetwork import NeuralNetwork from amp.model import LossFunction def generate_data(count, filename='training.traj'): """Generates test or training data with a simple MD simulation.""" if os.path.exists(filename): return traj = ase.io.Trajectory(filename, 'w') atoms = fcc110('Pt', (2, 2, 2), vacuum=7.) atoms.extend(Atoms([Atom('Cu', atoms[7].position + (0., 0., 2.5)), Atom('Cu', atoms[7].position + (0., 0., 5.))])) atoms.set_constraint(FixAtoms(indices=[0, 2])) atoms.set_calculator(EMT()) atoms.get_potential_energy() traj.write(atoms) MaxwellBoltzmannDistribution(atoms, 300. * units.kB) dyn = VelocityVerlet(atoms, dt=1. * units.fs) for step in range(count - 1): dyn.run(50) traj.write(atoms) generate_data(20) calc = Amp(descriptor=Gaussian(), model=NeuralNetwork(hiddenlayers=(10, 10, 10))) calc.model.lossfunction = LossFunction(convergence={'energy_rmse': 0.02, 'force_rmse': 0.02}) calc.train(images='training.traj') Note you can monitor the progress of the training by typing `amp-plotconvergence amp-log.txt`, which will create a file called `convergence.pdf`. ---------------------------------- Examining fingerprints ---------------------------------- With the modular nature, it's straightforward to analyze how fingerprints change with changes in images. The below script makes an animated GIF that shows how a fingerprint about the O atom in water changes as one of the O-H bonds is stretched. Note that most of the lines of code below are either making the atoms or making the figure; very little effort is needed to produce the fingerprints themselves---this is done in three lines. .. code-block:: python # Make a series of images. import numpy as np from ase.structure import molecule from ase import Atoms atoms = molecule('H2O') atoms.rotate('y', -np.pi/2.) atoms.set_pbc(False) displacements = np.linspace(0.9, 8.0, 20) vec = atoms[2].position - atoms[0].position images = [] for displacement in displacements: atoms = Atoms(atoms) atoms[2].position = (atoms[0].position + vec * displacement) images.append(atoms) # Fingerprint using Amp. from amp.descriptor.gaussian import Gaussian descriptor = Gaussian() from amp.utilities import hash_images images = hash_images(images, ordered=True) descriptor.calculate_fingerprints(images) # Plot the data. from matplotlib import pyplot def barplot(hash, name, title): """Makes a barplot of the fingerprint about the O atom.""" fp = descriptor.fingerprints[hash][0] fig, ax = pyplot.subplots() ax.bar(range(len(fp[1])), fp[1]) ax.set_title(title) ax.set_ylim(0., 2.) ax.set_xlabel('fingerprint') ax.set_ylabel('value') fig.savefig(name) for index, hash in enumerate(images.keys()): barplot(hash, 'bplot-%02i.png' % index, '%.2f$\\times$ equilibrium O-H bondlength' % displacements[index]) # For fun, make an animated gif. import os filenames = ['bplot-%02i.png' % index for index in range(len(images))] command = ('convert -delay 100 %s -loop 0 animation.gif' % ' '.join(filenames)) os.system(command) .. image:: _static/animation.gif :width: 600 px :align: center andrewpeterson-amp-4878fc892f2c/docs/gaussian.rst000066400000000000000000000024771332417112400217540ustar00rootroot00000000000000.. _Gaussian: Gaussian descriptor =================== Custom parameters ----------------- The Gaussian descriptor creates feature vectors based on the Behler scheme, and defaults to values used in Nano Letters 14:2670, 2014. You can specify custom parameters for the elements of the feature vectors as listed in the documentation of the :class:`~amp.descriptor.gaussian.Gaussian` class. There is also a helper function :func:`~amp.descriptor.gaussian.make_symmetry_functions` within the :mod:`amp.descriptor.gaussian` module to assist with this. An example of making a custom fingerprint is given below for a two-element system. .. code-block:: python import numpy as np from amp import Amp from amp.descriptor.gaussian import Gaussian, make_symmetry_functions from amp.model.neuralnetwork import NeuralNetwork elements = ['Cu', 'Pt'] G = make_symmetry_functions(elements=elements, type='G2', etas=np.logspace(np.log10(0.05), np.log10(80.), num=4)) G += make_symmetry_functions(elements=elements, type='G4', etas=[0.005], zetas=[1., 4.], gammas=[+1., -1.]) G = {'Cu': G, 'Pt': G} calc = Amp(descriptor=Gaussian(Gs=G), model=NeuralNetwork()) andrewpeterson-amp-4878fc892f2c/docs/index.rst000066400000000000000000000040561332417112400212440ustar00rootroot00000000000000.. Amp documentation master file, created by sphinx-quickstart on Thu Jul 30 17:27:50 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Amp: Atomistic Machine-learning Package ======================================= Amp is an open-source package designed to easily bring machine-learning to atomistic calculations. This project is being developed at Brown University in the School of Engineering, primarily by **Andrew Peterson** and **Alireza Khorshidi**, and is released under the GNU General Public License. The latest stable release of Amp is version 0.6, released on July 31, 2017; see the :ref:`ReleaseNotes` page for a download link. Please see the project's `git repository `_ for the latest development version or a place to report an issue. You can read about Amp in the below paper; if you find this project useful, we would appreciate if you cite this work: Khorshidi & Peterson, "Amp: A modular approach to machine learning in atomistic simulations", *Computer Physics Communications* 207:310-324, 2016. |amp_paper| .. |amp_paper| raw:: html DOI:10.1016/j.cpc.2016.05.010 **News**: An amp-users mailing list has been started, for general discussions about the use and development of Amp. You can subscribe via listserv at: https://listserv.brown.edu/?A0=AMP-USERS **Manual**: .. toctree:: :maxdepth: 1 introduction.rst installation.rst useamp.rst community.rst theory.rst credits.rst releasenotes.rst examplescripts.rst analysis.rst building.rst moredescriptor.rst moremodel.rst gaussian.rst tensorflow.rst databases.rst develop.rst **Module autodocumentation**: .. toctree:: :maxdepth: 1 modules/main.rst modules/descriptor.rst modules/model.rst modules/regression.rst modules/utilities.rst modules/analysis.rst **Indices and tables** * :ref:`genindex` * :ref:`modindex` * :ref:`search` andrewpeterson-amp-4878fc892f2c/docs/installation.rst000066400000000000000000000147331332417112400226410ustar00rootroot00000000000000.. _install: ================================== Installation ================================== AMP is python-based and is designed to integrate closely with the `Atomic Simulation Environment `_ (ASE). In its most basic form, it has few requirements: * Python, version 2.7 is recommended (it also supports Python3). * ASE. * NumPy. * SciPy. To get more features, such as parallelization in training, a few more packages are recommended: * Pexpect (or pxssh) * ZMQ (or PyZMQ, the python version of ØMQ). Certain advanced modules may contain dependencies that will be noted when they are used; for example Tensorflow for the tflow module or matplotlib for the plotting modules. Basic installation instructions follow. ---------------------------------- Install ASE ---------------------------------- We always test against the latest version (svn checkout) of ASE, but slightly older versions (>=3.9) are likely to work as well. Follow the instructions at the `ASE `_ website. ASE itself depends upon python with the standard numeric and scientific packages. Verify that you have working versions of `NumPy `_ and `SciPy `_. We also recommend `matplotlib `_ in order to generate plots. ---------------------------------- Get the code ---------------------------------- The latest stable release of Amp is version 0.5, which is permanently available at `https://doi.org/10.5281/zenodo.322427 `_. If installing version 0.5, you should follow ignore the rest of this page and follow the instructions included with the download (see docs/installation.rst or look for v0.5 on `http://amp.readthedocs.io `_). We are constantly improving *Amp* and adding features, so depending on your needs it may be preferable to use the development version rather than "stable" releases. We run daily unit tests to try to make sure that our development code works as intended. We recommend checking out the latest version of the code via `the project's bitbucket page `_. If you use git, check out the code with:: $ cd ~/path/to/my/codes $ git clone git@bitbucket.org:andrewpeterson/amp.git where you should replace '~/path/to/my/codes' with wherever you would like the code to be located on your computer. If you do not use git, just download the code as a zip file from the project's `download `_ page, and extract it into '~/path/to/my/codes'. Please make sure that the folder '~/path/to/my/codes/amp' includes subdirectories 'amp', 'docs', 'tests', and 'tools'. ---------------------------------- Set the environment ---------------------------------- You need to let your python version know about the existence of the amp module. Add the following line to your '.bashrc' (or other appropriate spot), with the appropriate path substituted for '~/path/to/my/codes':: $ export PYTHONPATH=~/path/to/my/codes/amp:$PYTHONPATH You can check that this works by starting python and typing the below command, verifying that the location listed from the second command is where you expect:: >>> import amp >>> print(amp.__file__) See also the section on parallel processing for any issues that arise in making the environment work with Amp in parallel. --------------------------------------- Recommended step: Build fortran modules --------------------------------------- Amp works in pure python, however, it will be annoyingly slow unless the associated Fortran 90 modules are compiled to speed up several parts of the code. The compilation of the Fortran 90 code and integration with the python parts is accomplished with f2py, which is part of NumPy. A Fortran 90 compiler will also be necessary on the system; a reasonable open-source option is GNU Fortran, or gfortran. This compiler will generate Fortran modules (.mod). gfortran will also be used by f2py to generate extension module fmodules.so on Linux or fmodules.pyd on Windows. We have included a `Make` file that automatizes the building of Fortran modules. To use it, install `GNU Makefile `_ on your Linux distribution or macOS. For Python2, then simply do:: $ cd /amp/ $ make python2 For Python3:: $ cd /amp/ $ make python3 If you do not have the GNU Makefile installed, you can prepare the Fortran extension modules manually in the following steps: 1. Compile model Fortran subroutines inside the model and descriptor folders by:: $ cd /amp/model $ gfortran -c neuralnetwork.f90 $ cd ../descriptor $ gfortran -c cutoffs.f90 2. Move the modules "neuralnetwork.mod" and "cutoffs.mod" created in the last step, to the parent directory by:: $ cd .. $ mv model/neuralnetwork.mod . $ mv descriptor/cutoffs.mod . 3. Compile the model Fortran subroutines in companion with the descriptor and neuralnetwork subroutines by something like:: $ f2py -c -m fmodules model.f90 descriptor/cutoffs.f90 descriptor/gaussian.f90 descriptor/zernike.f90 model/neuralnetwork.f90 Note that for Python3, you need to use `f2py3` instead of `f2py`. or on a Windows machine by:: $ f2py -c -m fmodules model.f90 descriptor/cutoffs.f90 descriptor/gaussian.f90 descriptor/zernike.f90 model/neuralnetwork.f90 --fcompiler=gnu95 --compiler=mingw32 Note that if you update your code (e.g., with 'git pull origin master') and the fortran code changes but your version of fmodules.f90 is not updated, an exception will be raised telling you to re-compile your fortran modules. ---------------------------------- Recommended step: Run the tests ---------------------------------- We include tests in the package to ensure that it still runs as intended as we continue our development; we run these tests on the latest build every night to try to keep bugs out. It is a good idea to run these tests after you install the package to see if your installation is working. The tests are in the folder `tests`; they are designed to run with `nose `_. If you have nose and GNU Makefile installed, simply do:: $ make py2tests (for Python2) $ make py3tests (for Python3) Otherwise, if you have only nose installed (and not GNU Makefile), run the commands below:: $ mkdir /tmp/amptests $ cd /tmp/amptests $ nosetests ~/path/to/my/codes/amp/tests andrewpeterson-amp-4878fc892f2c/docs/introduction.rst000066400000000000000000000022271332417112400226540ustar00rootroot00000000000000.. _introduction: ================================== Introduction ================================== Amp is an open-source package designed to easily bring machine-learning to atomistic calculations. This allows one to predict (or really, interpolate) calculations on the potential energy surface, by first building up a regression representation from a "training set" of atomic images. The Amp calculator works by first learning from any other calculator (usually quantum mechanical calculations) that can provide energy and forces as a function of atomic coordinates. Depending upon the model choice, the predictions from Amp can take place with arbitrary accuracy, approaching that of the original calculator. Amp is designed to integrate closely with the `Atomic Simulation Environment `_ (ASE). As such, the interface is in pure python, although several compute-heavy parts of the underlying codes also have fortran versions to accelerate the calculations. The close integration with ASE means that any calculator that works with ASE - including EMT, GPAW, DACAPO, VASP, NWChem, and Gaussian - can easily be used as the parent method. andrewpeterson-amp-4878fc892f2c/docs/modules/000077500000000000000000000000001332417112400210465ustar00rootroot00000000000000andrewpeterson-amp-4878fc892f2c/docs/modules/analysis.rst000066400000000000000000000002731332417112400234250ustar00rootroot00000000000000Analysis ============== Tools for analysis of output exist here. Module contents --------------- .. automodule:: amp.analysis :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/modules/descriptor.rst000066400000000000000000000012341332417112400237560ustar00rootroot00000000000000Descriptor ============== The descriptor module contains methods for describing the local atomic environment; that is, feature fectors that can be fed to machine-learning modules. Gaussian -------- .. automodule:: amp.descriptor.gaussian :members: :undoc-members: :show-inheritance: Zernike ------- .. automodule:: amp.descriptor.zernike :members: :undoc-members: :show-inheritance: Bispectrum ---------- .. automodule:: amp.descriptor.bispectrum :members: :undoc-members: :show-inheritance: Cutoff functions ---------------- .. automodule:: amp.descriptor.cutoffs :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/modules/main.rst000066400000000000000000000002661332417112400225300ustar00rootroot00000000000000Main ============== This module is the main part of the Amp package. Module contents --------------- .. automodule:: amp :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/modules/model.rst000066400000000000000000000013251332417112400227010ustar00rootroot00000000000000Model ===== This module is designed to include machine-learning models for interpolating energies and forces from either an atom-centered or image-centered fingerprint description. Model ----- .. automodule:: amp.model :members: :undoc-members: :show-inheritance: Neural Network -------------- .. automodule:: amp.model.neuralnetwork :members: :undoc-members: :show-inheritance: Tensorflow Neural Network ------------------------- A work in progress, this module `amp.model.tflow` uses Google's TensorFlow package to implement a neural network, which may provide GPU acceleration and other advantages. .. automodule:: amp.model.tflow :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/modules/regression.rst000066400000000000000000000003751332417112400237650ustar00rootroot00000000000000Regression ============== This module includes a regressor object used to optimize the parameters of the machine-learning model. Module contents --------------- .. automodule:: amp.regression :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/modules/utilities.rst000066400000000000000000000003471332417112400236170ustar00rootroot00000000000000Utilities ============== This module contains utilities for use with various aspects of the Amp calculator. Module contents --------------- .. automodule:: amp.utilities :members: :undoc-members: :show-inheritance: andrewpeterson-amp-4878fc892f2c/docs/moredescriptor.rst000066400000000000000000000020221332417112400231650ustar00rootroot00000000000000.. _MoreDescriptor: ================================== More on descriptors ================================== ---------------------------------- Fingerprint ranges ---------------------------------- It is often useful to examine your fingerprints more closely. There is a utility that can help with that, an example of its use is below. This assumes you have open a calculator called "calc.amp" and you want to examine the fingerprint ranges for your training data. .. code-block:: python from ase import io from amp.descriptor.analysis import FingerprintPlot from amp import Amp calc = Amp.load('calc.amp') images = io.read('training.traj', index=':') fpplot = FingerprintPlot(calc) fpplot(images) This will create a plot that looks something like below, here showing the fingerprint ranges for the specified element. .. image:: _static/fpranges.svg :width: 1000 px :align: center You can also overlay a specific image's fingerprint on to the fingerprint plot by using the `overlay` keyword when calling fpplot. andrewpeterson-amp-4878fc892f2c/docs/moremodel.rst000066400000000000000000000040261332417112400221150ustar00rootroot00000000000000.. _MoreModel: ================================== More on models ================================== Visualizing neural network outputs ---------------------------------- It can be useful to visualize the neural network model to see how it is behaving. For example, you may find nodes that are effectively shut off (e.g., always giving a constant value like 1) or that are acting as a binary switch (e.g., only returning 1 or -1). There is a tool to allow you to visualize the node outputs of a set of data. .. code-block:: python from amp.model.neuralnetwork import NodePlot nodeplot = NodePlot(calc) nodeplot.plot(images, filename='nodeplottest.pdf') This will create a plot that looks something like below. Note that one such series of plots is made for each element. Here, Layer 0 is the input layer, from the fingerprints. Layer 1 and Layer 2 are the hidden layers. Layer 3 is the output layer; that is, the contribution of Pt to the potential energy (before it is multiplied by and added to a parameter to bring it to the correct magnitude). .. image:: _static/nodeplot-Pt.svg :width: 1000 px :align: center Calling an observer during training ----------------------------------- It can be useful to call a function known as an "observer" during the training of the model. In the neural network implementation, this can be accomplished by attaching an observer directly to the model. The observer is executed at each call to `model.get_loss`, and is fed the arguments (self, vector, loss). An example of using the observer to print out one component of the parameter vector is shown below: .. code-block:: python def observer(model, vector, loss): """Prints out the first component of the parameter vector.""" print(vector[0]) calc.model.observer = observer calc.train(images) With this approach, all kinds of fancy tricks are possible, like calling *another* Amp model that reports the loss function on a test set of images. This could be useful to implement training with early stopping, for example. andrewpeterson-amp-4878fc892f2c/docs/releasenotes.rst000066400000000000000000000052641332417112400226300ustar00rootroot00000000000000.. _ReleaseNotes: Release notes ============= 0.6.1 ----- Release date: July 19, 2018 * Updated to allow installation via pip. 0.6 --- Release date: July 31, 2017 * Python 3 compatibility. Following the release of python3-compatible ASE, we decided to jump on the wagon ourselves. The code should still work fine in python 2.7. (The exception is the tensorflow module, which still only lives inside python 2, unfortunately.) * A community page has been added with resources such as the new mailing list and issue tracker. * The default convergence parameters have been changed to energy-only training; force-training can be added by the user via the loss function. This makes convergence easier for new users. * Convergence plots show maximum residuals as well as root mean-squared error. * Parameters to make the Gaussian feature vectors are now output to the log file. * The helper function :func:`~amp.descriptor.gaussian.make_symmetry_functions` has been added to more easily customize Gaussian fingerprint parameters. Permanently available at https://doi.org/10.5281/zenodo.836788 0.5 --- Release date: February 24, 2017 The code has been significantly restructured since the previous version, in order to increase the modularity; much of the code structure has been changed since v0.4. Specific changes below: * A parallelization scheme allowing for fast message passing with ZeroMQ. * A simpler database format based on files, which optionally can be compressed to save diskspace. * Incorporation of an experimental neural network model based on google's TensorFlow package. Requires TensorFlow version 0.11.0. * Incorporation of an experimental bootstrap module for uncertainty analysis. Permanently available at https://doi.org/10.5281/zenodo.322427 0.4 --- Release date: February 29, 2016 Corresponds to the publication of Khorshidi, A; Peterson*, AA. Amp: a modular approach to machine learning in atomistic simulations. Computer Physics Communications 207:310-324, 2016. http://dx.doi.org/10.1016/j.cpc.2016.05.010 Permanently available at https://doi.org/10.5281/zenodo.46737 0.3 --- Release date: July 13, 2015 First release under the new name "Amp" (Atomistic Machine-Learning Package/Potentials). Permanently available at https://doi.org/10.5281/zenodo.20636 0.2 --- Release date: July 13, 2015 Last version under the name "Neural: Machine-learning for Atomistics". Future versions are named "Amp". Available as the v0.2 tag in https://bitbucket.org/andrewpeterson/neural/commits/tag/v0.2 0.1 --- Release date: November 12, 2014 (Package name: Neural: Machine-Learning for Atomistics) Permanently available at https://doi.org/10.5281/zenodo.12665. First public bitbucket release: September, 2014. andrewpeterson-amp-4878fc892f2c/docs/tensorflow.rst000066400000000000000000000071721332417112400223410ustar00rootroot00000000000000.. _TensorFlow: ================================== TensorFlow ================================== Google has released an open-source version of its machine-learning software named Tensorflow, which can allow for efficient backpropagation of neural networks and utilization of GPUs for extra speed. We have incorporated an experimental module that uses a tensorflow back-end, which may provide an acceleration particularly through access to GPU systems. As of this writing, the tensorflow code is in flux (with version 1.0 anticipated shortly). Dependencies --------------------------------- This package requires google's TensorFlow 0.11.0. You can install it as shown below for Linux:: export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.11.0-cp27-none-linux_x86_64.whl pip install -U --upgrade $TF_BINARY_URL or macOS:: export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-0.11.0-py2-none-any.whl pip install -U --upgrade $TF_BINARY_URL If you want more information, please see `tensorflow's website `_ for instructions for installation on your system. Example --------------------------------- .. code-block:: python #!/usr/bin/env python """Simple test of the Amp calculator, using Gaussian descriptors and neural network model. Randomly generates data with the EMT potential in MD simulations.""" from ase.calculators.emt import EMT from ase.lattice.surface import fcc110 from ase import Atoms, Atom from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ase import units from ase.md import VelocityVerlet from ase.constraints import FixAtoms from amp import Amp from amp.descriptor.gaussian import Gaussian from amp.model.tflow import NeuralNetwork def generate_data(count): """Generates test or training data with a simple MD simulation.""" atoms = fcc110('Pt', (2, 2, 2), vacuum=7.) adsorbate = Atoms([Atom('Cu', atoms[7].position + (0., 0., 2.5)), Atom('Cu', atoms[7].position + (0., 0., 5.))]) atoms.extend(adsorbate) atoms.set_constraint(FixAtoms(indices=[0, 2])) atoms.set_calculator(EMT()) MaxwellBoltzmannDistribution(atoms, 300. * units.kB) dyn = VelocityVerlet(atoms, dt=1. * units.fs) newatoms = atoms.copy() newatoms.set_calculator(EMT()) newatoms.get_potential_energy() images = [newatoms] for step in range(count - 1): dyn.run(50) newatoms = atoms.copy() newatoms.set_calculator(EMT()) newatoms.get_potential_energy() images.append(newatoms) return images def train_test(): label = 'train_test/calc' train_images = generate_data(2) convergence = { 'energy_rmse': 0.02, 'force_rmse': 0.02 } calc = Amp(descriptor=Gaussian(), model=NeuralNetwork(hiddenlayers=(3, 3), convergenceCriteria=convergence), label=label, cores=1) calc.train(images=train_images,) for image in train_images: print "energy =", calc.get_potential_energy(image) print "forces =", calc.get_forces(image) if __name__ == '__main__': train_test() Known issues --------------------------------- - `tflow` module does not work for versions different from 0.11.0. About --------------------------------- This module was contributed by Zachary Ulissi (Department of Chemical Engineering, Stanford University, zulissi@gmail.com) with help, testing, and discussions from Andrew Doyle (Stanford) and the Amp development team. andrewpeterson-amp-4878fc892f2c/docs/theory.rst000066400000000000000000000255011332417112400214450ustar00rootroot00000000000000.. _theory: ================================== Theory ================================== According to the Born-Oppenheimer approximation, the ground-state potential energy of an atomic configuration is dictated solely by the nuclear coordinates (under certain conditions, such as the absence of external fields and constant charge). The potential energy is in general a very complicated function of the nuclear coordinates; it in theory can be calculated by directly solving the Schrodinger equation. However, in practice, an exact analytical solution to the many-body Schrodinger equation is very difficult (if not impossible), and most electronic structure codes provide a point-by-point approximation to the ground-state potential energy for given nuclear configurations. Given enough example calculations from any electronic structure calculator, the idea is then to approximate the potential energy with a regression model: .. math:: \textbf{R}\xrightarrow{\text{regression}}E=E(\textbf{R}), where :math:`\textbf{R}` is the position of atoms in the system. ----------------------------------------- Atomic representation of potential energy ----------------------------------------- In order to have a potential function which is simultaneously applicable to systems of different sizes, the total potential energy of the system can to be broken up into atomic energy contributions: .. math:: E(\textbf{R})=\sum_{\text{atom}=1}^{N}E_\text{atom}(\textbf{R}). The above expansion can be justified by assembling the atomic configuration by bringing atoms close to each other one by one. Then the atomic energy contributions (instead of the energy of the whole system at once) can be approximated using a regression method: .. math:: \textbf{R}\xrightarrow{\text{regression}}E_\text{atom}=E_\text{atom}\left(\textbf{R}\right). ---------- Descriptor ---------- A better interpolation can be achieved if an appropriate symmetry function :math:`\textbf{G}` of atomic coordinates, approximating the functional dependence of local energetics, is used as the input of the regression operator: .. math:: \textbf{R}\xrightarrow{\textbf{G}}\textbf{G}\left(\textbf{R}\right)\xrightarrow{\text{regression}}E_\text{atom}=E_\text{atom}\left(\textbf{G}\left(\textbf{R}\right)\right). ******** Gaussian ******** A Gaussian descriptor :math:`\textbf{G}` as a function of pair-atom distances and three-atom angles has been suggested by Behler [1], and is implemented within Amp. Radial fingerprints of the Gaussian type capture the interaction of atom :math:`i` with all atoms :math:`j` as the sum of Gaussians with width :math:`\eta` and center :math:`R_s`, .. math:: G_{i}^{I}=\sum^{\tiny{\begin{array}{c} \text{atoms j within }R_c\\ \text{ distance of atom i} \end{array}}}_{j\ne i}{e^{-\eta(R_{ij}-R_s)^2/R_c^2}f_c\left(R_{ij}\right)}. By specifying many values of :math:`\eta` and :math:`R_s` we can begin to build a feature vector for regression. The next type is the angular fingerprint accounting for three-atom interactions. The Gaussian angular fingerprints are computed for all triplets of atoms :math:`i`, :math:`j`, and :math:`k` by summing over the cosine values of the angles :math:`\theta_{ijk}=\cos^{-1}\left(\displaystyle\frac{\textbf{R}_{ij}.\textbf{R}_{ik}}{R_{ij}R_{ik}}\right)`, (:math:`\textbf{R}_{ij}=\textbf{R}_{i}-\textbf{R}_{j}`), centered at atom :math:`i`, according to .. math:: G_{i}^{II}=2^{1-\zeta}\sum^{\tiny{\begin{array}{c} \text{atoms j, k within }R_c\\ \text{ distance of atom i} \end{array}}}_{\scriptsize\begin{array}{c} j,\,k\ne i \\ (j\ne k) \end{array}}{\left(1+\lambda \cos \theta_{ijk}\right)^\zeta e^{-\eta\left(R_{ij}^2+R_{ik}^2+R_{jk}^2\right)/R_c^2}f_c\left(R_{ij}\right)f_c\left(R_{ik}\right)f_c\left(R_{jk}\right)}, with parameters :math:`\lambda`, :math:`\eta`, and :math:`\zeta`, which again can be chosen to build more elements of a feature vector. The cutoff function :math:`f_c\left(R_{ij}\right)` in the above equations defines the energetically relevant local environment with value one at :math:`R_{ij}=0` and zero at :math:`R_{ij}=R_{c}`, where :math:`R_c` is the cutoff radius. In order to have a continuous force-field, the cutoff function :math:`f_c\left(R_{ij}\right)` as well as its first derivative should be continuous in :math:`R_{ij}\in\left[0,\infty\right)`. One possible expression for such a function as proposed by Behler [1] is .. math:: f_{c}\left(r\right)== \begin{cases} &0.5\left(1+\cos\left(\pi\displaystyle\frac{r}{R_c}\right)\right)\qquad \text{for}\;\: r\leq R_{c},\\ &0\qquad\qquad\qquad\qquad\quad\quad\quad\:\: \text{for}\;\: r> R_{c}.\\ \end{cases} Another more general choice for the cutoff function is the following polynomial [5]: .. math:: f_{c} \left( r \right)= \begin{cases} 1 + \gamma \cdot \left(r/R_c\right)^{\gamma + 1} - (\gamma + 1) \left(r/R_c\right)^{\gamma}\qquad\quad &\text{if}\;\: r\leq R_{c},\\ 0&\text{if}\;\: r> R_{c},\\ \end{cases} with a user-specified parameter :math:`\gamma` that determines the rate of decay of the cutoff function as it extends from :math:`r=0` to :math:`r=R_c`. The figure below shows how components of the fingerprints :math:`\textbf{G}_{i}^{I}` and :math:`\textbf{G}_{i}^{II}` change with, respectively, distance :math:`R_{ij}` between the pair of atoms :math:`i` and :math:`j` and the valence angle :math:`\theta_{ijk}` between the triplet of atoms :math:`i`, :math:`j`, and :math:`k` with central atom :math:`i`: .. image:: _static/gaussian.svg :width: 800 px :align: center ******* Zernike ******* A three-dimensional Zernike descriptor is also available inside Amp, and can be used as the atomic environment descriptor. The Zernike-type descriptor has been previously used in the machine-learning community extensively, but it has been suggested here for the first time for representing the local chemical environment. Zernike moments are basically a tensor product between spherical harmonics (complete and orthogonal on the surface of the unit sphere), and Zernike polynomials (complete and orthogonal within the unit sphere). Zernike descriptor components for each integer degree are then defined as the norm of Zernike moments with the same corresponding degree. For more details on the Zernike descriptor the reader is referred to the nice paper of Novotni and Klein [2]. Inspired by Bartok et. al. [3], to represent the local chemical environment of atom :math:`i`, an atomic density function :math:`\rho_{i}(\mathbf{r})` is defined for each atomic local environment as the sum of delta distributions shifted to atomic positions: .. math:: \rho_{i}(\mathbf{r}) = \sum_{j\neq i}^{\tiny{\begin{array}{c} \text{atoms j within }R_c\\ \text{ distance of atom i} \end{array}}}\eta_{j}\delta\left(\mathbf{r}-\mathbf{R}_{ij}\right)f_{c}\left(\|\mathbf{R}_{ij}\|\right), Next, components of the Zernike descriptor are computed from Zernike moments of the above atomic density destribution for each atom :math:`i`. The figure below shows how components of the Zernike descriptor vary with pair-atom distance, three-atom angle, and four-atom dehidral angle. It is important to note that components of the Gaussian descriptor discussed above are non-sensitive to the four-atom dehidral angle of the following figure. .. image:: _static/zernike.svg :width: 1200 px :align: center ********** Bispectrum ********** Bispectrum of four-dimensional spherical harmonics have been suggested by Bartok et al. [3] to be invariant under rotation of the local atomic environment. In this approach, the atomic density distribution defined above is first mapped onto the surface of unit sphere in four dimensions. Consequently, Bartok et al. have shown that the bispectrum of this mapping can be used as atomic environment descriptor. We refer the reader to the original paper [3] for mathematical details. This approach of describing local environment is also available inside Amp. ---------------- Regression Model ---------------- The general purpose of the regression model :math:`x\xrightarrow{\text{regression}}y` with input :math:`x` and output :math:`y` is to approximate the function :math:`y=f(x)` by using sample training data points :math:`(x_i, y_i)`. The intent is to later use the approximated :math:`f` for input data :math:`x_j` (other than :math:`x_i` in the training data set), and make predictions for :math:`y_j`. Typical regression models include Gaussian processes, support vector regression, and neural network. ******************** Neural network model ******************** A neural network model is basically a very simple model of how the nervous system processes information. The first mathematical model was developed in 1943 by McCulloch and Pitts [4] for classification purposes; biological neurons either send or do not send a signal to the neighboring neuron. The model was soon extended to do linear and nonlinear regression, by replacing the binary activation function with a continuous function. The basic functional unit of a neural network is called "node". A number of parallel nodes constitute a layer. A feed-forward neural network consists of at least an input layer plus an output layer. When approximating the PES, the output layer has just one neuron representing the potential energy. For a more robust interpolation, a number of "hidden layers" may exist in the neural network as well; the word "hidden" refers to the fact that these layers have no physical meaning. A schematic of a typical feed-forward neural network is shown below. In each node a number of inputs is multiplied by the corresponding weights and summed up with a constant bias. An activation function then acts upon the summation and an output is generated. The output is finally sent to the neighboring neuron in the next layer. Typically used activation functions are hyperbolic tangent, sigmoid, Gaussian, and linear functions. The unbounded linear activation function is particularly useful in the last hidden layer to scale neural network outputs to the range of reference values. For our purpose, the output of neural network represents energy of atomic system. .. image:: _static/nn.svg :width: 500 px :align: center **References:** 1. "Atom-centered symmetry functions for constructing high-dimensional neural network potentials", J. Behler, J. Chem. Phys. 134(7), 074106 (2011) 2. "Shape retrieval using 3D Zernike descriptors", M. Novotni and R. Klein, Computer-Aided Design 36(11), 1047--1062 (2004) 3. "Gaussian approximation potentials: The accuracy of quantum mechanics, without the electrons", A.P. Bart\'ok, M.C. Payne, R. Kondor and G. Csanyi, Physical Review Letters 104, 136403 (2010) 4. "A logical calculus of the ideas immanent in nervous activity", W.S. McCulloch, and W.H. Pitts, Bull. Math. Biophys. 5, 115--133 (1943) 5. "Amp: A modular approach to machine learning in atomistic simulations", A. Khorshidi, and A.A. Peterson, Comput. Phys. Commun. 207, 310--324 (2016) andrewpeterson-amp-4878fc892f2c/docs/useamp.rst000066400000000000000000000262551332417112400214340ustar00rootroot00000000000000.. _UseAmp: ================================== Using Amp ================================== If you are familiar with ASE, the use of Amp should be intuitive. At its most basic, Amp behaves like any other ASE calculator, except that it has a key extra method, called `train`, which allows you to fit the calculator to a set of atomic images. This means you can use Amp as a substitute for an expensive calculator in any atomistic routine, such as molecular dynamics, global optimization, transition-state searches, normal-mode analyses, phonon analyses, etc. ---------------------------------- Basic use ---------------------------------- To use Amp, you need to specify a `descriptor` and a `model`. The below shows a basic example of training :class:`~amp.Amp` with :class:`~amp.descriptor.gaussian.Gaussian` descriptors and a :class:`~amp.model.neuralnetwork.NeuralNetwork` model---the Behler-Parinello scheme. .. code-block:: python from amp import Amp from amp.descriptor.gaussian import Gaussian from amp.model.neuralnetwork import NeuralNetwork calc = Amp(descriptor=Gaussian(), model=NeuralNetwork(), label='calc') calc.train(images='my-images.traj') After training is successful you can use your trained calculator just like any other ASE calculator (although you should be careful that you can only trust it within the trained regime). This will also result in the saving the calculator parameters to "