pax_global_header 0000666 0000000 0000000 00000000064 13324171124 0014510 g ustar 00root root 0000000 0000000 52 comment=4878fc892f2cbc5cd9f29f7a367d7b05bdeb6ee9
andrewpeterson-amp-4878fc892f2c/ 0000775 0000000 0000000 00000000000 13324171124 0016446 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/.gitignore 0000664 0000000 0000000 00000000044 13324171124 0020434 0 ustar 00root root 0000000 0000000 *.pyc
*.so
*.mod
*.o
*.py~
*.py.swp
andrewpeterson-amp-4878fc892f2c/LICENSE 0000664 0000000 0000000 00000104515 13324171124 0017461 0 ustar 00root root 0000000 0000000 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/README 0000664 0000000 0000000 00000004005 13324171124 0017325 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 13324171124 0017223 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/Makefile 0000664 0000000 0000000 00000001135 13324171124 0020663 0 ustar 00root root 0000000 0000000 python2:
+$(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/VERSION 0000664 0000000 0000000 00000000006 13324171124 0020267 0 ustar 00root root 0000000 0000000 0.6.1
andrewpeterson-amp-4878fc892f2c/amp/__init__.py 0000664 0000000 0000000 00000043247 13324171124 0021346 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000064435 13324171124 0021434 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 13324171124 0021401 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/descriptor/Makefile 0000664 0000000 0000000 00000000071 13324171124 0023037 0 ustar 00root root 0000000 0000000 cutoffs.mod:
gfortran -c cutoffs.f90
cp cutoffs.mod ..
andrewpeterson-amp-4878fc892f2c/amp/descriptor/__init__.py 0000664 0000000 0000000 00000000135 13324171124 0023511 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Folder that contains different local environment descriptors.
"""
andrewpeterson-amp-4878fc892f2c/amp/descriptor/analysis.py 0000664 0000000 0000000 00000007752 13324171124 0023611 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000062266 13324171124 0024144 0 ustar 00root root 0000000 0000000 import 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.f90 0000664 0000000 0000000 00000004057 13324171124 0023400 0 ustar 00root root 0000000 0000000 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.py 0000664 0000000 0000000 00000007431 13324171124 0023431 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000031234 13324171124 0023411 0 ustar 00root root 0000000 0000000 import 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.f90 0000664 0000000 0000000 00000051463 13324171124 0023544 0 ustar 00root root 0000000 0000000 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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.py 0000664 0000000 0000000 00000140626 13324171124 0023576 0 ustar 00root root 0000000 0000000 import 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.f90 0000664 0000000 0000000 00000031532 13324171124 0023374 0 ustar 00root root 0000000 0000000 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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.py 0000664 0000000 0000000 00000116045 13324171124 0023431 0 ustar 00root root 0000000 0000000 import 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.f90 0000664 0000000 0000000 00000102615 13324171124 0020650 0 ustar 00root root 0000000 0000000 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! 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/ 0000775 0000000 0000000 00000000000 13324171124 0020323 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/model/Makefile 0000664 0000000 0000000 00000000113 13324171124 0021756 0 ustar 00root root 0000000 0000000 neuralnetwork.mod:
gfortran -c neuralnetwork.f90
cp neuralnetwork.mod ..
andrewpeterson-amp-4878fc892f2c/amp/model/__init__.py 0000775 0000000 0000000 00000141276 13324171124 0022452 0 ustar 00root root 0000000 0000000 import 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__.py 0000664 0000000 0000000 00000010324 13324171124 0022415 0 ustar 00root root 0000000 0000000 """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.f90 0000664 0000000 0000000 00000217626 13324171124 0023561 0 ustar 00root root 0000000 0000000 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! 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.py 0000664 0000000 0000000 00000134706 13324171124 0023610 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000235653 13324171124 0022046 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 13324171124 0021403 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/regression/__init__.py 0000664 0000000 0000000 00000010206 13324171124 0023513 0 ustar 00root root 0000000 0000000 from ..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/ 0000775 0000000 0000000 00000000000 13324171124 0020361 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/stats/__init__.py 0000664 0000000 0000000 00000000000 13324171124 0022460 0 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/amp/stats/bootstrap.py 0000664 0000000 0000000 00000035671 13324171124 0022764 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000122325 13324171124 0021615 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 13324171124 0017376 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/docs/README 0000664 0000000 0000000 00000000167 13324171124 0020262 0 ustar 00root root 0000000 0000000 For instructions on how to build this documentation, please see the
"Documentation" section of the file "develop.rst".
andrewpeterson-amp-4878fc892f2c/docs/_static/ 0000775 0000000 0000000 00000000000 13324171124 0021024 5 ustar 00root root 0000000 0000000 andrewpeterson-amp-4878fc892f2c/docs/_static/amp-logo.svg 0000664 0000000 0000000 00000203122 13324171124 0023260 0 ustar 00root root 0000000 0000000
andrewpeterson-amp-4878fc892f2c/docs/_static/animation.gif 0000664 0000000 0000000 00000632352 13324171124 0023505 0 ustar 00root root 0000000 0000000 GIF89a Xõ&