diff --git a/.github/workflows/flare.yml b/.github/workflows/flare.yml index e1511d124..3c2d0aa93 100644 --- a/.github/workflows/flare.yml +++ b/.github/workflows/flare.yml @@ -16,7 +16,8 @@ jobs: matrix: omp: [OFF, ON] lapack: [OFF, ON] - name: "(OpenMP, Lapack) =" + python-version: ["3.7", "3.8"] + name: "(OpenMP, Lapack, Python) =" # The type of runner that the job will run on runs-on: ubuntu-latest @@ -30,35 +31,10 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - - name: Install Sphinx and Breathe - run: | - sudo apt-get update - sudo apt-get install python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-nbsphinx - - - name: Run Doxygen - uses: mattnotmitt/doxygen-action@v1.1.0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 with: - # Path to Doxyfile - doxyfile-path: "./Doxyfile" # default is ./Doxyfile - # Working directory - working-directory: "./docs" # default is . - - - name: Run Sphinx - run: | - cd docs - pwd - ls - make html - - - name: Publish the docs - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - # Default Doxyfile build documentation to html directory. - # Change the directory if changes in Doxyfile - publish_dir: ./docs/build/html - if: github.event_name == 'pull_request' && matrix.lapack == 'on' && matrix.omp == 'on' + python-version: ${{ matrix.python-version }} - name: Build run: | @@ -82,7 +58,7 @@ jobs: echo "Lapack ${{ matrix.lapack }}" cmake .. - cmake --build . -j + cmake --build . -j4 cp _C_flare* ../flare/bffs/sgp cd ctests ./tests @@ -90,6 +66,20 @@ jobs: - name: Install LAMMPS run: | git clone --depth 1 https://github.com/lammps/lammps.git lammps + + cd lammps/src + cp pair_hybrid.* pair_lj_cut.* .. + rm pair_*.cpp pair_*.h + mv ../pair_hybrid.* ../pair_lj_cut.* . + cp MANYBODY/pair_tersoff.* . + rm MANYBODY/pair_*.* + rm MANYBODY/fix_*.* + mv pair_tersoff.* MANYBODY/ + cp KOKKOS/pair_kokkos.* . + rm KOKKOS/pair_*.* + mv pair_kokkos.* KOKKOS/ + cd ../.. + cd lammps_plugins ./install.sh $(pwd)/../lammps cd .. @@ -97,12 +87,12 @@ jobs: cd lammps mkdir build cd build - cmake ../cmake -DPKG_KOKKOS=ON -DKokkos_ENABLE_OPENMP=ON -DPKG_MANYBODY=yes + cmake ../cmake -DPKG_KOKKOS=ON -DKokkos_ENABLE_OPENMP=ON -DPKG_MANYBODY=ON make -j4 - name: Pip install run: | - pip install -U codecov pytest pytest-cov pytest_mock + pip install -U codecov pytest pytest-cov pytest_mock Sphinx sphinx-rtd-theme breathe nbsphinx pip install -r requirements.txt - name: Patch ASE @@ -122,6 +112,35 @@ jobs: cd tests pytest test_lammps.py + - name: Install Sphinx and Breathe + run: | + sudo apt-get update + sudo apt-get install python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-nbsphinx + + - name: Run Doxygen + uses: mattnotmitt/doxygen-action@v1.1.0 + with: + # Path to Doxyfile + doxyfile-path: "./Doxyfile" # default is ./Doxyfile + # Working directory + working-directory: "./docs" # default is . + + - name: Run Sphinx + run: | + cd docs + pwd + ls + make html + + - name: Publish the docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Default Doxyfile build documentation to html directory. + # Change the directory if changes in Doxyfile + publish_dir: ./docs/build/html + if: github.event_name == 'pull_request' && matrix.lapack == 'on' && matrix.omp == 'on' + # - name: Run tutorial # run: | # cd tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 932331cbe..ef94cf93d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ set(FLARE_SOURCES src/flare_pp/descriptors/four_body.cpp src/flare_pp/kernels/kernel.cpp src/flare_pp/kernels/normalized_dot_product.cpp + src/flare_pp/kernels/dot_product.cpp src/flare_pp/kernels/norm_dot_icm.cpp src/flare_pp/kernels/squared_exponential.cpp) diff --git a/README.md b/README.md index 1ab6f7a8d..aed58f1a0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![Build Status](https://github.com/mir-group/flare/actions/workflows/main.yml/badge.svg)](https://github.com/mir-group/flare/actions) [![documentation](https://readthedocs.org/projects/flare/badge/?version=latest)](https://readthedocs.org/projects/flare) [![pypi](https://img.shields.io/pypi/v/mir-flare)](https://pypi.org/project/mir-flare/) [![activity](https://img.shields.io/github/commit-activity/m/mir-group/flare)](https://github.com/mir-group/flare/commits/master) [![codecov](https://codecov.io/gh/mir-group/flare/branch/master/graph/badge.svg)](https://codecov.io/gh/mir-group/flare) +[![Build Status](https://github.com/mir-group/flare/actions/workflows/flare.yml/badge.svg)](https://github.com/mir-group/flare/actions) [![pypi](https://img.shields.io/pypi/v/mir-flare)](https://pypi.org/project/mir-flare/) [![activity](https://img.shields.io/github/commit-activity/m/mir-group/flare)](https://github.com/mir-group/flare/commits/master) [![codecov](https://codecov.io/gh/mir-group/flare/branch/master/graph/badge.svg)](https://codecov.io/gh/mir-group/flare) + +***NOTE: This is the latest release [1.3.3](https://github.com/mir-group/flare/releases/tag/1.3.3) which includes significant changes compared to the previous version [0.2.4](https://github.com/mir-group/flare/releases/tag/0.2.4). Please check the updated tutorials and documentations from the links below.*** # FLARE: Fast Learning of Atomistic Rare Events @@ -88,14 +90,22 @@ pytest ``` ## References -- If you use FLARE++ including B2 descriptors, NormalizedDotProduct kernel and Sparse GP, please cite the following paper: +If you use FLARE++ including B2 descriptors, NormalizedDotProduct kernel and Sparse GP, please cite the following paper: - > [1] Vandermause, J., Xie, Y., Lim, J.S., Owen, C.J. and Kozinsky, B., 2021. *Active learning of reactive Bayesian force fields: Application to heterogeneous hydrogen-platinum catalysis dynamics.* [arXiv preprint arXiv:2106.01949](https://arxiv.org/abs/2106.01949). + > [1] Vandermause, J., Xie, Y., Lim, J.S., Owen, C.J. and Kozinsky, B., 2021. *Active learning of reactive Bayesian force fields: Application to heterogeneous hydrogen-platinum catalysis dynamics.* Nature Communications 13.1 (2022): 5183. https://www.nature.com/articles/s41467-022-32294-0 -- If you use FLARE active learning workflow, full Gaussian process or 2-body/3-body kernel in your research, please cite the following paper: +If you use FLARE active learning workflow, full Gaussian process or 2-body/3-body kernel in your research, please cite the following paper: > [2] Vandermause, J., Torrisi, S. B., Batzner, S., Xie, Y., Sun, L., Kolpak, A. M. & Kozinsky, B. *On-the-fly active learning of interpretable Bayesian force fields for atomistic rare events.* npj Comput Mater 6, 20 (2020). https://doi.org/10.1038/s41524-020-0283-z -- If you use FLARE LAMMPS pair style or MGP (mapped Gaussian process), please cite the following paper: +If you use FLARE LAMMPS pair style or MGP (mapped Gaussian process), please cite the following paper: > [3] Xie, Y., Vandermause, J., Sun, L. et al. *Bayesian force fields from active learning for simulation of inter-dimensional transformation of stanene.* npj Comput Mater 7, 40 (2021). https://doi.org/10.1038/s41524-021-00510-y + +If you use FLARE PyLAMMPS for training, please cite the following paper: + + > [4] Xie, Y., Vandermause, J., Ramakers, S., Protik, N.H., Johansson, A. and Kozinsky, B., 2022. *Uncertainty-aware molecular dynamics from Bayesian active learning: Phase Transformations and Thermal Transport in SiC.* npj Comput. Mater. 9(1), 36 (2023). + +If you use FLARE LAMMPS Kokkos pair style with GPU acceleration, please cite the following paper: + + > [5] Johansson, A., Xie, Y., Owen, C.J., Soo, J., Sun, L., Vandermause, J. and Kozinsky, B., 2022. *Micron-scale heterogeneous catalysis with Bayesian force fields from first principles and active learning.* arXiv preprint arXiv:2204.12573. diff --git a/ctests/test_kernels.cpp b/ctests/test_kernels.cpp index fc50d3a2b..97e33cf5e 100644 --- a/ctests/test_kernels.cpp +++ b/ctests/test_kernels.cpp @@ -19,7 +19,7 @@ class KernelTest : public StructureTest { }; -using KernelTypes = ::testing::Types; +using KernelTypes = ::testing::Types; TYPED_TEST_SUITE(KernelTest, KernelTypes); TYPED_TEST(KernelTest, TimeSelfKernel) { diff --git a/ctests/test_sparse_gp.cpp b/ctests/test_sparse_gp.cpp index 281b13091..bf996f29b 100644 --- a/ctests/test_sparse_gp.cpp +++ b/ctests/test_sparse_gp.cpp @@ -163,9 +163,11 @@ TEST_F(StructureTest, LikeGrad) { test_struc.forces = forces; test_struc.stresses = stresses; - sparse_gp.add_training_structure(test_struc); + sparse_gp.add_training_structure(test_struc, {-1}, 0.4, 0.2, 0.3); sparse_gp.add_all_environments(test_struc); + // TODO: add another test structure + EXPECT_EQ(sparse_gp.Sigma.rows(), 0); EXPECT_EQ(sparse_gp.Kuu_inverse.rows(), 0); @@ -194,8 +196,9 @@ TEST_F(StructureTest, LikeGrad) { like_down = sparse_gp.compute_likelihood_gradient(hyps_down); fin_diff = (like_up - like_down) / (2 * pert); + printf("like_grad=%lg, fin_diff=%lg\n", like_grad(i), fin_diff); - EXPECT_NEAR(like_grad(i), fin_diff, 1e-3 * abs(fin_diff)); + EXPECT_NEAR(like_grad(i), fin_diff, 5e-3 * abs(fin_diff)); } } @@ -230,8 +233,19 @@ TEST_F(StructureTest, LikeGradStable) { test_struc.forces = forces; test_struc.stresses = stresses; - sparse_gp.add_training_structure(test_struc, {0, 1, 3, 5}); + test_struc_2 = Structure(cell_2, species_2, positions_2, cutoff, dc); + + Eigen::VectorXd energy_2 = Eigen::VectorXd::Random(1); + Eigen::VectorXd forces_2 = Eigen::VectorXd::Random(n_atoms * 3); + Eigen::VectorXd stresses_2 = Eigen::VectorXd::Random(6); + test_struc_2.energy = energy_2; + test_struc_2.forces = forces_2; + test_struc_2.stresses = stresses_2; + + sparse_gp.add_training_structure(test_struc, {-1}, 0.4, 0.2, 0.3); sparse_gp.add_specific_environments(test_struc, {0, 1, 3}); + sparse_gp.add_training_structure(test_struc_2, {0, 1, 3, 5}, 0.64, 0.55, 0.45); + sparse_gp.add_specific_environments(test_struc_2, {2, 3, 4}); EXPECT_EQ(sparse_gp.Sigma.rows(), 0); EXPECT_EQ(sparse_gp.Kuu_inverse.rows(), 0); @@ -282,8 +296,8 @@ TEST_F(StructureTest, LikeGradStable) { fin_diff = (like_up - like_down) / (2 * pert); std::cout << like_grad(i) << " " << fin_diff << std::endl; - EXPECT_NEAR(like_grad(i), fin_diff, 1e-5); - EXPECT_NEAR(like_grad(i), like_grad_original(i), 1e-6); + EXPECT_NEAR(like_grad(i), fin_diff, 1e-5 * abs(fin_diff)); + EXPECT_NEAR(like_grad(i), like_grad_original(i), 1e-6 * abs(fin_diff)); } } @@ -320,14 +334,14 @@ TEST_F(StructureTest, Set_Hyps) { // Add sparse environments and training structures. std::cout << "adding training structure" << std::endl; - sparse_gp_1.add_training_structure(test_struc); - sparse_gp_1.add_training_structure(test_struc_2); + sparse_gp_1.add_training_structure(test_struc, {-1}, 0.1, 0.2, 0.3); + sparse_gp_1.add_training_structure(test_struc_2, {-1}, 0.5, 0.4, 0.6); sparse_gp_1.add_all_environments(test_struc); sparse_gp_1.add_all_environments(test_struc_2); std::cout << "adding training structure" << std::endl; - sparse_gp_2.add_training_structure(test_struc); - sparse_gp_2.add_training_structure(test_struc_2); + sparse_gp_2.add_training_structure(test_struc, {-1}, 0.1, 0.2, 0.3); + sparse_gp_2.add_training_structure(test_struc_2, {-1}, 0.5, 0.4, 0.6); sparse_gp_2.add_all_environments(test_struc); sparse_gp_2.add_all_environments(test_struc_2); diff --git a/ctests/test_structure.h b/ctests/test_structure.h index d3597a1c0..8442f2aaa 100644 --- a/ctests/test_structure.h +++ b/ctests/test_structure.h @@ -5,6 +5,7 @@ #include "structure.h" #include "four_body.h" #include "normalized_dot_product.h" +#include "dot_product.h" #include "norm_dot_icm.h" #include "squared_exponential.h" #include "structure.h" diff --git a/docs/source/citing.rst b/docs/source/citing.rst index 637a5be34..458a319ca 100644 --- a/docs/source/citing.rst +++ b/docs/source/citing.rst @@ -3,7 +3,7 @@ How to Cite If you use FLARE++ including B2 descriptors, NormalizedDotProduct kernel and Sparse GP, please cite the following paper: - [1] Vandermause, J., Xie, Y., Lim, J.S., Owen, C.J. and Kozinsky, B., 2021. *Active learning of reactive Bayesian force fields: Application to heterogeneous hydrogen-platinum catalysis dynamics.* [arXiv preprint arXiv:2106.01949](https://arxiv.org/abs/2106.01949). + [1] Vandermause, J., Xie, Y., Lim, J.S., Owen, C.J. and Kozinsky, B., 2021. *Active learning of reactive Bayesian force fields: Application to heterogeneous hydrogen-platinum catalysis dynamics.* Nature Communications 13.1 (2022): 5183. https://www.nature.com/articles/s41467-022-32294-0 If you use FLARE active learning workflow, full Gaussian process or 2-body/3-body kernel in your research, please cite the following paper: @@ -13,4 +13,12 @@ If you use FLARE LAMMPS pair style or MGP (mapped Gaussian process), please cite [3] Xie, Y., Vandermause, J., Sun, L. et al. *Bayesian force fields from active learning for simulation of inter-dimensional transformation of stanene.* npj Comput Mater 7, 40 (2021). https://doi.org/10.1038/s41524-021-00510-y +If you use FLARE PyLAMMPS for training, please cite the following paper: + + [4] Xie, Y., Vandermause, J., Ramakers, S., Protik, N.H., Johansson, A. and Kozinsky, B., 2022. *Uncertainty-aware molecular dynamics from Bayesian active learning: Phase Transformations and Thermal Transport in SiC.* npj Comput. Mater. 9(1), 36 (2023). + +If you use FLARE LAMMPS Kokkos pair style with GPU acceleration, please cite the following paper: + + [5] Johansson, A., Xie, Y., Owen, C.J., Soo, J., Sun, L., Vandermause, J. and Kozinsky, B., 2022. *Micron-scale heterogeneous catalysis with Bayesian force fields from first principles and active learning.* arXiv preprint arXiv:2204.12573. + Thank you for using FLARE! diff --git a/docs/source/installation/install.rst b/docs/source/installation/install.rst index ea549ffca..b13534696 100644 --- a/docs/source/installation/install.rst +++ b/docs/source/installation/install.rst @@ -14,24 +14,24 @@ Requirements 2. Use conda to install compilers and dependencies for flare -* Option 1: If you want to install flare with mkl +* Option 1: You can load modules if your machine have already installed them (with mkl) + +.. code-block:: bash + + module load cmake/3.17.3 gcc/9.3.0 intel-mkl/2017.2.174 + +* Option 2: If you want to install flare with mkl .. code-block:: bash conda install -y gcc gxx cmake mkl-devel mkl-service mkl_fft openmp -c conda-forge -* Option 2: If you want to install flare with openblas + lapacke +* Option 3: If you want to install flare with openblas + lapacke .. code-block:: bash conda install -y gcc gxx cmake openmp liblapacke openblas -c conda-forge -* Option 3: You can load modules if your machine have already installed them - -.. code-block:: bash - - module load cmake/3.17.3 gcc/9.3.0 intel-mkl/2017.2.174 - 3. Download flare code from github repo and pip install .. code-block:: bash @@ -107,6 +107,13 @@ Trouble shooting * the `ldd` command above should show the linked libraries in the `.conda/envs/flare` directory +* If you encounter `Intel MKL FATAL ERROR` when running flare (after the compilation has done), this is likely a static library linkage issue. You can set up the environmental variable + +.. code-block:: bash + + export LD_PRELOAD=${CONDA_PREFIX}/lib/libmkl_core.so:${CONDA_PREFIX}/lib/libmkl_intel_thread.so:${CONDA_PREFIX}/lib/libiomp5.so + +as instructed in `this discussion `_. ***************************************** Acceleration with multiprocessing and MKL diff --git a/docs/source/related.rst b/docs/source/related.rst index 7dc70d023..4e812cd55 100644 --- a/docs/source/related.rst +++ b/docs/source/related.rst @@ -39,17 +39,21 @@ We will list the applications of FLARE here. - Claudio Zeni, Kevin Rossi, Theodore Pavloudis, Joseph Kioseoglou, Stefano de Gironcoli, Richard Palmer, and Francesca Baletto. **Data-driven simulation and characterization of gold nanoparticles melting.** Nat Commun 12, 6056 (2021). (`arXiv `_) (`published version `_) .. figure:: https://media.springernature.com/full/springer-static/image/art%3A10.1038%2Fs41467-021-26199-7/MediaObjects/41467_2021_26199_Fig3_HTML.png - :figwidth: 80 % + :figwidth: 60 % :align: center Melting of Au nanoparticle - Kai Xu, Lei Yan, and Bingran You. **Bayesian active learning of interatomic force field for molecular dynamics simulation of Pt/Ag(111).** ChemRxiv preprint. (`ChemRxiv `_) +.. figure:: https://www.linkpicture.com/q/Screenshot-2023-02-23-at-2.36.23-PM.png + :figwidth: 80 % + :align: center + - Anders Johansson, Yu Xie, Cameron J. Owen, Jin Soo Lim, Lixin Sun, Jonathan Vandermause, Boris Kozinsky. **Micron-scale heterogeneous catalysis with Bayesian force fields from first principles and active learning** (`arXiv `_) .. figure:: https://i.imgur.com/CPCrgWl.png - :figwidth: 80 % + :figwidth: 60 % :align: center Strong scaling benchmarks @@ -62,8 +66,46 @@ We will list the applications of FLARE here. - Harry H. Halim and Yoshitada Morikawa. **Elucidation of Cu–Zn Surface Alloying on Cu(997) by Machine-Learning Molecular Dynamics.** ACS Phys. Chem Au (2022). (`published version `_) -.. figure:: https://pubs.acs.org/cms/10.1021/acsphyschemau.2c00017/asset/images/large/pg2c00017_0020.jpeg +.. figure:: https://www.linkpicture.com/q/pg2c00017_0020.png :figwidth 80 % :align: center Cu-Zn surface alloying + +- Yu Xie, Jonathan Vandermause, Senja Ramakers, Nakib H. Protik, Anders Johansson, Boris Kozinsky. **Uncertainty-aware molecular dynamics from Bayesian active learning: Phase Transformations and Thermal Transport in SiC.** arXiv:2203.03824. (`arXiv `_) + +.. figure:: https://d3i71xaburhd42.cloudfront.net/775fb27655f25e1b1ff46ce9bda5f77a3c86abdf/8-Figure3-1.png + :figwidth: 80 % + :align: center + + SiC phase transition + +- Zhou, Chen, Hio Tong Ngan, Jin Soo Lim, Zubin Darbari, Adrian Lewandowski, Dario J. Stacchiola, Boris Kozinsky, Philippe Sautet, and Jorge Anibal Boscoboinik. **Dynamical Study of Adsorbate-Induced Restructuring Kinetics in Bimetallic Catalysts Using the PdAu (111) Model System.** Journal of the American Chemical Society 144, no. 33 (2022): 15132-15142. + +.. figure:: https://www.linkpicture.com/q/ja2c04871_0006.png + :figwidth: 80 % + :align: center + +- Hong, Sung Jun, Hoje Chun, Jehyun Lee, Byung-Hyun Kim, Min Ho Seo, Joonhee Kang, and Byungchan Han. **First-principles-based machine-learning molecular dynamics for crystalline polymers with van der Waals interactions.** The Journal of Physical Chemistry Letters 12, no. 25 (2021): 6000-6006. + +.. figure:: https://www.linkpicture.com/q/jz1c01140_0006.png + :figwidth: 80 % + :align: center + +- Duschatko, Blake R., Jonathan Vandermause, Nicola Molinari, and Boris Kozinsky. **Uncertainty Driven Active Learning of Coarse Grained Free Energy Models.** arXiv preprint arXiv:2210.16364 (2022). + +.. figure:: https://www.linkpicture.com/q/Screenshot-2023-02-23-at-2.57.53-PM.png + :figwidth: 80 % + :align: center + +- Cameron J Owen, Steven B Torrisi, Yu Xie, Simon Batzner, Jennifer Coulter, Albert Musaelian, Lixin Sun, Boris Kozinsky. **Complexity of Many-Body Interactions in Transition Metals via Machine-Learned Force Fields from the TM23 Data Set.** arXiv preprint arXiv:2302.12993 (2023). + +.. figure:: https://www.linkpicture.com/q/Screenshot-2023-03-15-at-3.41.52-PM.png + :figwidth: 80 % + :align: center + +- Mike Pols, Victor Brouwers, Sofía Calero, Shuxia Tao. **How fast do defects migrate in halide perovskites: insights from on-the-fly machine-learned force fields.** Chemical Communications 59, no. 31 (2023): 4660-4663. + +.. figure:: https://www.linkpicture.com/q/Get.jpeg + :figwidth: 80 % + :align: center diff --git a/docs/source/tutorials/after_training.ipynb b/docs/source/tutorials/after_training.ipynb index 0503424e6..4420a054d 100644 --- a/docs/source/tutorials/after_training.ipynb +++ b/docs/source/tutorials/after_training.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# After Training\n", + "# Build 2+3-body Mapped GP\n", "\n", "After the on-the-fly training is complete, we can play with the force field we obtained. \n", "We are going to do the following things:\n", diff --git a/docs/source/tutorials/aps_tutorial.ipynb b/docs/source/tutorials/aps_tutorial.ipynb deleted file mode 100644 index 0a41dd2b5..000000000 --- a/docs/source/tutorials/aps_tutorial.ipynb +++ /dev/null @@ -1,2513 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "q8tB4lvLyJt5" - }, - "source": [ - "## Introduction to FLARE: Fast Learning of Atomistic Rare Events\n", - "Jonathan Vandermause (jonathan_vandermause@g.harvard.edu)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "juyRGASPwpZp" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "EM1Q0tPDyV2n" - }, - "source": [ - "**Learning objectives:**\n", - " * Train 2+3-body Gaussian process models on _ab initio_ force data.\n", - " * Use the uncertainties of the GP to train a force field on the fly." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "X7beT1bu0Raw" - }, - "source": [ - "Computing the properties of real materials with quantum mechanical accuracy is extremely expensive. One of the most efficient explicitly quantum mechanical tools we have for the job, density functional theory (or DFT), has a computational cost that scales cubically with the number of atoms in the system, and is therefore limited in practice to at most a few hundred atoms (with some hope of scaling further with orbital free DFT methods).\n", - "\n", - "One of the most popular strategies for getting around this is to simply ignore the hard part of the problem---the electrons---and to instead try to model the system with an _empirical interatomic potential_, which expresses the potential energy as a sum over local, atom-centered contributions that depend only on atomic coordinates. The resulting model scales only linearly with the number of atoms, making it possible to simulate the behavior of hundreds of thousands or even millions of atoms over microsecond timescales. The problem then becomes: how do you design a good potential? Is it possible to find a fast empirical potential with the accuracy of DFT?\n", - "\n", - "FLARE is an open-source software that uses Bayesian machine learning to try to bridge the gap between accurate but slow quantum methods (like DFT) and fast but inaccurate classical methods (like empirical interatomic potentials). The idea is to train fast potentials on accurate DFT data, and ideally to do so in an automatic, closed loop fashion, so that a wide class of materials and material compositions can be efficiently explored. The key tool FLARE uses to accomplish this is Gaussian process (GP) regression, an elegant framework for defining probability distributions over functions. In this tutorial, we'll explore the use of GPs to model interactions between atoms in materials.\n", - "\n", - "If you're interested in learning more about the FLARE code, check out our [paper](https://www.nature.com/articles/s41524-020-0283-z), [GitHub page](https://github.com/mir-group/flare), and [code documentation](https://flare.readthedocs.io/en/latest/).\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "XsHT0ojhOHzK" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KjgnY7xyMbqN" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "vGnyg-MZveLF" - }, - "source": [ - "### Installation" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "HyKryc-Mvkf5" - }, - "source": [ - "We can install FLARE and its dependencies here in Google Colab. (This will take about a minute.)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "j081chMvvjGM" - }, - "outputs": [], - "source": [ - "! pip install --upgrade mir-flare" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "teewYv-1wKaC" - }, - "source": [ - "Let's check that it worked by trying to import FLARE:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "johWDfKEwMW_" - }, - "outputs": [], - "source": [ - "import flare" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GMXBZ-1iwXCo" - }, - "source": [ - "If you don't see an error, you're all set for the tutorial! Let's also go ahead and import all the modules we'll need now:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "g9ny8zihdStP" - }, - "outputs": [], - "source": [ - "from flare import gp, struc, output, predict, md, otf, env,\\\n", - " otf_parser\n", - "from flare.kernels import mc_simple\n", - "from flare.utils import md_helper\n", - "\n", - "import numpy as np\n", - "import time\n", - "import matplotlib.pyplot as plt\n", - "import pickle\n", - "from ase.visualize import view" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6Tc3YD4awVNJ" - }, - "source": [ - "### Machine learned force fields" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "alU2W7TJc5Lf" - }, - "source": [ - "#### Training data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GsTCK8eE7k_O" - }, - "source": [ - "Let's start by downloading some _ab initio_ force data to play with." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "jlDbThFGw7-0" - }, - "outputs": [], - "source": [ - "# download ab initio MD data\n", - "! wget https://zenodo.org/record/3688843/files/AgI_data.zip?download=1\n", - "\n", - "# unzip the folder\n", - "! unzip AgI_data.zip?download=1" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d0OSf5RynISB" - }, - "source": [ - "The folder \"AgI_data\" contains data from an _ab initio_ molecular dynamics simulation of the fast-ion conductor silver iodide (AgI), where _ab initio_ means \"from the beginning\" or \"from first principles\". In other words, we simulate how the atoms move by calculating the quantum mechanical forces on the ions at every timestep using DFT. To give you a sense of how expensive that is, the ~2500 timesteps in this simulation (about 12 picoseconds of simulation time) required 2 days of wall time on 256 cpus. (And there were only 32 atoms in the simulation!)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yizt6MKVnpP7" - }, - "source": [ - "Let's load the positions, forces, cell, and species of the atoms in the simulation." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "POnrwXgnoHme" - }, - "outputs": [], - "source": [ - "# load AIMD training data\n", - "data_directory = 'AgI_data/'\n", - "species = np.load(data_directory + 'species.npy') # atomic numbers of the atoms\n", - "positions = np.load(data_directory + 'positions.npy') # in angstrom (A)\n", - "cell = np.load(data_directory + 'cell.npy') # 3x3 array of cell vectors (in A)\n", - "forces = np.load(data_directory + 'forces.npy') # in eV/A" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "1EKBa7D5tg48" - }, - "source": [ - "#### Training a GP model." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "uVS0cGEO7f76" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DT3K2ZAsOefO" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "egbSwBOHO679" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "XqV1CwOedM2a" - }, - "source": [ - "Let's train a GP model on the data. We'll first initialize a Gaussian process object, which involves choosing a kernel function and its gradient, an initial set of hyperparameters, and the cutoff radii of local environments. We'll use a 2+3-body kernel, which compares local environments by comparing the pairs and triplets of atoms inside the environments." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "HyS9uo3HuFHc" - }, - "outputs": [], - "source": [ - "# create a 2+3-body gaussian process object\n", - "kernels = ['twobody', 'threebody']\n", - "component = 'mc'\n", - "hyps = np.array([0.1, 0.1, 0.1, 2.0, 0.5]) # initial (bad) choice of hyps\n", - "cutoffs = {'twobody': 7.0, 'threebody': 5.5} # cutoff radii in A\n", - "maxiter = 100 # max number of hyperparameter optimziation steps\n", - "\n", - "gp_model = gp.GaussianProcess(\n", - " kernels=kernels,\n", - " component=component,\n", - " hyps=hyps,\n", - " cutoffs=cutoffs,\n", - " maxiter=50\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yHaJeGwGeVB0" - }, - "source": [ - "Let's put a couple of structures into the training set." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "iaEZ-DCTu6Z9" - }, - "outputs": [], - "source": [ - "# put a few snapshots in the training set\n", - "snapshots = [500, 1500]\n", - "for snapshot in snapshots:\n", - " # create flare structure\n", - " training_positions = positions[snapshot]\n", - " training_forces = forces[snapshot]\n", - " training_structure = struc.Structure(cell, species, training_positions)\n", - "\n", - " # add the structure to the training set of the GP\n", - " gp_model.update_db(training_structure, training_forces)\n", - "\n", - "gp_model.set_L_alpha()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_-TBgAFahaJZ" - }, - "source": [ - "The \"set_L_alpha\" method updates the covariance matrix of the GP, computes the alpha vector used to make predictions, and computes the log marginal likelihood of the training data. Let's see what that looks like: " - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "id": "sRH0jj1YekaW", - "outputId": "348c6e77-ef37-4348-89f1-24294693c4b5" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-491.75669961275526\n" - ] - } - ], - "source": [ - "print(gp_model.likelihood)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xYcSuYmehyNI" - }, - "source": [ - "This is too low, suggesting that our initial guess for the hyperparameters was not a good one." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "-YYU3fQFh8t2" - }, - "source": [ - "#### Task: Find hyperparameters that give a positive log marginal likelihood." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "LX78w6jgiKP0" - }, - "source": [ - "Hint: For the 2+3-body kernel, the hyperparameters are in the following order:\n", - "* Signal variance of the 2-body kernel (in eV)\n", - "* Length scale of the 2-body kernel (in A)\n", - "* Signal variance of the 3-body kernel (in eV)\n", - "* Length scale of the 3-body kernel (in A)\n", - "* Noise hyperparameter (in eV/A)\n", - "\n", - "What is a plausible length scale for this problem? Note that machine learned force fields typically have errors in the range 50-200 meV/A, and the energy of a pair or triplet is much less than the total local energy assigned to an atom." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9V1OB1BJii1U" - }, - "outputs": [], - "source": [ - "# your code here\n", - "\n", - "# hint: reset the hyperparameters with gp_model.hyps = *new hyps*, then use\n", - "# set_L_alpha to recompute the likelihood" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "GeS6kyP8jBLX" - }, - "source": [ - "#### Solution" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_y3CcbPRjHjH" - }, - "source": [ - "A reasonable guess for the length scale is 1 A, and since errors are often around 0.1 eV/A, we'll choose that as our noise level. The signal variances require some tuning, but we should expect them to be significantly less than 1 eV, with the triplet contribution significantly smaller than the pair contribution." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "id": "nZmaqP6XjNYR", - "outputId": "5d6b246b-f38a-41e6-bfdd-8c7537625c70" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "9.500574018895804\n" - ] - } - ], - "source": [ - "gp_model.hyps = np.array([0.01, 1, 0.001, 1, 0.2])\n", - "gp_model.set_L_alpha()\n", - "print(gp_model.likelihood)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "EfAsmRWVkYtS" - }, - "source": [ - "#### Optimizing the hyperparameters rigorously" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Xq2sZI1PkdkS" - }, - "source": [ - "The above was an exercise in manual hyperparameter tuning, which is tedious and should be avoided if possible. Because we can compute the gradient of the likelihood with respect to the hyperparameters, we can instead use a more principled gradient descent approach to find the best set of hyperparameters." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "7slPPfgwebdA" - }, - "outputs": [], - "source": [ - "# optimize the hyperparameters (this will take some time!)\n", - "gp_model.train(print_progress=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yMYXaRb8lLmV" - }, - "source": [ - "In case you don't want to wait, here are some good values:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 34 - }, - "id": "T4lKRPJQlsLm", - "outputId": "9a297ba1-423f-43b4-e72f-893d7ea18b4a" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "71.16991245736048\n" - ] - } - ], - "source": [ - "gp_model.hyps = np.array([1.59612454e-02, 5.70104540e-01, 6.01290125e-04,\n", - " 9.17243358e-01, 1.09666317e-01])\n", - "gp_model.set_L_alpha()\n", - "print(gp_model.likelihood)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "k86d8fRy11fL" - }, - "source": [ - "#### Calculating the learning curve" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "_GHQ9495nDNh" - }, - "source": [ - "Let's get a feel for how much data the model needs by computing the learning curve, i.e. the performance on a validation set as a function of the number of training points. Let's create training and validation structures drawn from the AIMD simulation." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "BoUg2GzBqcjW" - }, - "outputs": [], - "source": [ - "# choose a training snapshot\n", - "training_snapshot = 500\n", - "training_positions = positions[training_snapshot]\n", - "training_forces = forces[training_snapshot]\n", - "training_structure = struc.Structure(cell, species, training_positions)\n", - "\n", - "# choose a validation snapshot\n", - "validation_snapshot = 2300\n", - "validation_positions = positions[validation_snapshot]\n", - "validation_forces = forces[validation_snapshot]\n", - "validation_structure = struc.Structure(cell, species, validation_positions)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "NAMI7wJwrH5B" - }, - "source": [ - "We now loop over the atomic environments in the training structure and add them one by one to the training set of the GP model. After adding an environment, we predict all the forces on the validation structure and compute the MAE. We'll also time the prediction step to get a feel for how the cost of GP predictions depends on the size of the training set." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 578 - }, - "id": "jvpor2DOsly_", - "outputId": "28d01b22-3115-46a4-e7fb-943edf88ab79" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "computing validation errors...\n", - "0.315073761272364\n", - "0.29588515439108337\n", - "0.3234313322939279\n", - "0.29323785059806706\n", - "0.20129532813447323\n", - "0.19131142668143372\n", - "0.1827709765647784\n", - "0.17610478136490326\n", - "0.16902120241436183\n", - "0.17466172091548085\n", - "0.16829757387913633\n", - "0.16823160886145053\n", - "0.1797266370933175\n", - "0.17570045468126091\n", - "0.1778018841113225\n", - "0.16181457527423726\n", - "0.15221658747132247\n", - "0.151463002400943\n", - "0.15330262545396417\n", - "0.14366990594229426\n", - "0.13955981466966869\n", - "0.1386958938185947\n", - "0.13337032134291107\n", - "0.13856470639127907\n", - "0.13934854695295343\n", - "0.1384439054077334\n", - "0.1376182149699538\n", - "0.1398980197468981\n", - "0.13878505117526346\n", - "0.13947592439212986\n", - "0.14140188721074384\n", - "0.14095157040791634\n" - ] - } - ], - "source": [ - "# reset the gp with hyperparameters fixed to the optimized values\n", - "hyps_final = gp_model.hyps\n", - "gp_model = gp.GaussianProcess(\n", - " kernels=kernels,\n", - " component=component,\n", - " hyps=hyps,\n", - " cutoffs=cutoffs,\n", - " maxiter=50\n", - ")\n", - "gp_model.hyps = hyps_final\n", - "\n", - "# add atomic environments one by one to the training set\n", - "n_atoms = 32 # number of atoms in the structure\n", - "validation_errors = np.zeros(n_atoms)\n", - "prediction_times = np.zeros(n_atoms)\n", - "validation_count = 0\n", - "\n", - "print('computing validation errors...')\n", - "for n, atom in enumerate(range(n_atoms)):\n", - " # add the current atomic environment to the training set\n", - " gp_model.update_db(training_structure, training_forces,\n", - " custom_range=[atom])\n", - " gp_model.set_L_alpha()\n", - "\n", - " # predict the forces on the validation structure\n", - " time0 = time.time()\n", - " pred_forces, stds = \\\n", - " predict.predict_on_structure(validation_structure, gp_model)\n", - " time1 = time.time()\n", - "\n", - " mae = np.mean(np.abs(pred_forces - validation_forces))\n", - " validation_errors[validation_count] = mae\n", - " prediction_times[validation_count] = time1 - time0\n", - " validation_count += 1\n", - "\n", - " print(mae)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "z5GLpahJt5jU" - }, - "source": [ - "Let's make a plot of the results." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 542 - }, - "id": "P6u24jtwsBuT", - "outputId": "540fe26f-163e-43c2-f1bc-12e4ac985ea7" - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(validation_errors)\n", - "plt.xlabel('number of training environments')\n", - "plt.ylabel('mae on validation structure (eV/$\\AA$)')\n", - "plt.show()\n", - "\n", - "plt.plot(prediction_times)\n", - "plt.xlabel('number of training environments')\n", - "plt.ylabel('prediction wall time (s)')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "wsJUUVBiuBPZ" - }, - "source": [ - "Notice that the prediction time grows roughly linearly with the number of training environments." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "dR-0VO_K7N-r" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "f1pSbziN7RWf" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ox6phwrb7UPn" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PCFBHloyw_wD" - }, - "source": [ - "### Learning a force field on the fly" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xHs1LGUs71-8" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4Q4fWYdcRQO9" - }, - "source": [ - "#### The Lennard Jones Potential" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "J0NkHAJ-RTOw" - }, - "source": [ - "In production FLARE runs, we make calls to a DFT solver like Quantum Espresso, VASP, or CP2K whenever the uncertainty on a force component is unacceptably high. In principle, the forces can come from anywhere, and FLARE allows users to write a custom solver that returns forces on an arbitrary input structure.\n", - "\n", - "Here, we'll define a simple custom solver that returns Lennard Jones forces, and then try to reconstruct the potential on the fly.\n", - "\n", - "The Lennard Jones potential takes the following form:\n", - "\\begin{equation}\n", - "V_{\\text{LJ}} = 4\\epsilon \\left[ \\left(\\frac{\\sigma}{r} \\right)^{12} - \\left(\\frac{\\sigma}{r}\\right)^{6} \\right].\n", - "\\end{equation}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "MgcREyDYUvZn" - }, - "source": [ - "Let's define our custom solver. To do this, we need to create a class with two methods:\n", - "\n", - "* parse_dft_input: Takes the name of an input file, and returns the positions, species, cell, and masses of the atoms specified in the input file.\n", - "\n", - "* run_dft_par: Takes a FLARE structure and an optional dictionary of keywords, and returns the forces on the atoms." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "uR0vNVOruNv2" - }, - "source": [ - "We'll make a multi-species Lennard Jones function, which allows different values of $\\sigma$ and $\\epsilon$ for different pairs of species. We'll store these as 2-D numpy arrays, so that, e.g., the $\\sigma$ value assigned to species 1 and 3 is stored in element (1, 3) of this array." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rjYaUuIUY1LV" - }, - "source": [ - "The derivative of the Lennard Jones potential with respect to $r$ is:\n", - "\n", - "\\begin{equation}\n", - "\\frac{dV_{\\text{LJ}}}{dr} = 4\\epsilon \\left[ -\\frac{12 \\sigma^{12}}{r^{13}} +\\frac{6 \\sigma^6}{r^7} \\right].\n", - "\\end{equation}" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "id": "Ha1km1AIXzI1" - }, - "outputs": [], - "source": [ - "def get_LJ_forces(structure, lj_parameters):\n", - " \"\"\"Calculate multicomponent Lennard Jones forces on a structure of atoms.\n", - " dft_kwargs is assumed to be a dictionary containing the cutoff, an ordered\n", - " list of species, and arrays containing epsilon and sigma values.\"\"\"\n", - "\n", - " cutoff = lj_parameters['cutoff']\n", - " epsilons = lj_parameters['epsilons']\n", - " sigmas = lj_parameters['sigmas']\n", - " spec_list = lj_parameters['species']\n", - "\n", - " forces = np.zeros((structure.nat, 3))\n", - " # Loop over atoms in the structure.\n", - " for m in range(structure.nat):\n", - " # Create atomic environment.\n", - " environment = env.AtomicEnvironment(structure, m, np.array([cutoff]))\n", - " ind1 = spec_list.index(environment.ctype)\n", - "\n", - " # Loop over atoms in the environment to compute the total force.\n", - " for n in range(len(environment.etypes)):\n", - " ind2 = spec_list.index(environment.etypes[n])\n", - " eps = epsilons[ind1, ind2]\n", - " sig = sigmas[ind1, ind2]\n", - "\n", - " # Compute LJ force.\n", - " bond_vals = environment.bond_array_2[n]\n", - " r = bond_vals[0]\n", - "\n", - " dE_dr = 4 * eps * (-12 * sig ** 12 / (r ** 13) +\n", - " 6 * sig ** 6 / (r ** 7))\n", - "\n", - " forces[m, 0] += dE_dr * bond_vals[1]\n", - " forces[m, 1] += dE_dr * bond_vals[2]\n", - " forces[m, 2] += dE_dr * bond_vals[3]\n", - "\n", - " return forces" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "S4Zy-VyRfRtG" - }, - "source": [ - "We can use our Lennard Jones force function to define a custom module." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "id": "ptX61cWyxBtA" - }, - "outputs": [], - "source": [ - "# create lj module\n", - "class lj_module:\n", - " def parse_dft_input(file_name):\n", - " \"\"\"We assume the input is a pickled dictionary containing positions\n", - " (in angstrom), species (as a list of strings), cell (as a 3x3 matrix of\n", - " cell vectors), and masses (as a dictionary assigning each species a mass\n", - " in AMU).\"\"\"\n", - "\n", - " input_file = open(file_name, 'rb')\n", - " struc_dict = pickle.load(input_file)\n", - " input_file.close()\n", - "\n", - " # Convert masses to MD units (energy = eV, length = A, )\n", - " masses = struc_dict['masses']\n", - " mass_convert = 0.000103642695727\n", - " for species in masses:\n", - " masses[species] *= mass_convert\n", - "\n", - " return struc_dict['positions'], struc_dict['species'], \\\n", - " struc_dict['cell'], masses\n", - "\n", - " def run_dft_par(dft_input=None, structure=None, dft_loc=None, n_cpus=None,\n", - " npool=None, mpi=None, dft_kwargs=None, dft_out=None):\n", - " return get_LJ_forces(structure, dft_kwargs)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "id": "_8H4oLv94KDF" - }, - "outputs": [], - "source": [ - "# create dictionary of lj parameters\n", - "cutoff = 5.\n", - "epsilons = np.array([[3, 2.5], [2.5, 3.]])\n", - "sigmas = np.array([[2.0, 1.9], [1.9, 2.1]])\n", - "spec_list = [47, 53] # silver and iodine atomic numbers\n", - "\n", - "lj_params = {'cutoff': cutoff, 'epsilons': epsilons, 'sigmas': sigmas,\n", - " 'species': spec_list}" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 289 - }, - "id": "wvo_ntrinEyK", - "outputId": "e8c7ad99-11a2-4cbf-8752-35f46de8af96" - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZAAAAEQCAYAAACeDyIUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOy9eXxU9fX//3xPtkkmySSTfSUbBEiAAGETEQEVBPe6IG6ttdZ9oa3a6s9vF631U2utFYtLbdW6FW3FhUW0uLAo+5YQshGykn3fM/P+/XGTALJm7k0yIe/n4zGPO5m5c+ZMCPc155z3OW8hpUShUCgUir5iGmwHFAqFQjE0UQKiUCgUCqdQAqJQKBQKp1AColAoFAqnUAKiUCgUCqdQAqJQKBQKp3BJARFCuAkhdgohPhlsXxQKhUJxYlxSQID7gf2D7YRCoVAoTo7LCYgQIhpYBLw62L4oFAqF4uS4D7YDJ+A54CHA72QnCCFuB24HsFgsk0ePHn1qi82VUF8M4ePApO8jd9kl+w83EBngTZDF02k7jR2NFDYWkmhNxOxu1uWTQqFQ9JXt27dXSSlD9NhwKQERQlwCVEgptwshzj/ZeVLKl4GXAdLT0+W2bdtObXjfB/D+rXDXOxA6RpePHV0ORj22mqUXjuLeeSOdtrOrYhc3rb6J5RcsZ2bUTF0+KRQKRV8RQhzSa8PVUlgzgcuEEAXAu8BcIcS/dFv1CdaOzVW6TXm6m/DzcqempUOXHZvZBkBNW41unxQKhWIwcCkBkVL+UkoZLaWMAxYD/5NS3qjbsKVbQFr0CwhAgMWDupZOXTaUgCgUiqGOSwlIv2FgBAJg8/GkpllfBGLxsOBp8qS6rdoQnxQKhWKgcakayNFIKb8EvjTEmI/2bZ8WYy7WgRb9AiKEwOZto6ZVRSAK/XR2dlJcXExbW9tgu6JwMcxmM9HR0Xh4eBhu22UFxFDcPMAcYGgEklvRpN+O2aZSWApDKC4uxs/Pj7i4OIQQg+2OwkWQUlJdXU1xcTHx8fGG2x8eKSzQ6iAG1UCCfD2pbtIXgYASEIVxtLW1ERQUpMRDcQxCCIKCgvotMh0+AuITbFwEYvGitdNOS0eXPjtKQBQGosRDcSL68+9i+AiIJdiwGkhPA6HeKMRmtlHbVovaVlihUAxFho+A+AQZFoEE+WoCoreQbjPbaLO30drVaoRbCoXLcdttt5GZmdkvtltaWli0aBGjR48mJSWFRx555KTnPvXUUyQlJZGcnMzatWv7xR8jWb58OePGjSMtLY1zzz33pL/DNWvWkJycTFJSEn/4wx8G2MvhJCA9EYjDoduUrScCaW7XZ6e7F0Qt5VWcrbz66quMHTu23+z//Oc/Jysri507d7Jx40ZWr1593DmZmZm8++67ZGRksGbNGu666y7sdnu/+WQES5YsYe/evezatYuHHnqIpUuXHneO3W7n7rvvZvXq1WRmZvLOO+/0m1ifjOEjID7BIO3QVqfbVLCvF2BMCgugulUJiGJo09zczKJFi5gwYQKpqam89957AJx//vn0jBr6+9//zqhRo5g6dSo/+clPuOeee3S9p4+PD3PmzAHA09OTSZMmUVxcfNx5K1euZPHixXh5eREfH09SUhJbtmw5pe3t27cze/ZsJk+ezPz58ykrK+v9PPfffz9paWmkpqb22vnqq69IS0sjLS2NiRMn0tjYqOuz+fv7995vbm4+YR1jy5YtJCUlkZCQgKenJ4sXL2blypW63revDI9lvHBUN3r1kb4QJ+mJQPSmsIK8gzQ7qpCuMJDffJxBZmmDoTbHRvrz/y5NOenza9asITIykk8//RSA+vr6Y54vLS3ld7/7HTt27MDPz4+5c+cyYcKE4+ysX7+eBx988LjHfXx82LRp00nfv66ujo8//pj777//uOdKSkqYPn1678/R0dGUlJSc1FZnZyf33nsvK1euJCQkhPfee49HH32U1157DdBSZ7t27eLrr7/m1ltvZd++fTzzzDMsW7aMmTNn0tTUhNl8/IDUWbNmnVBYnnnmGS644ILjHl+2bBnPPvssHR0d/O9//zvh54qJiTnmc3333Xcn/Vz9wfAREB/tYk1zFQQ7PwQRwMfTDS93E9U6BSTYWxO1qlZjajMKxWAxbtw4fvazn/Hwww9zySWXMGvWrGOe37JlC7Nnz8Zm0768XXPNNWRnZx9nZ86cOezatatP793V1cX111/PfffdR0JCgvMfopsDBw6wb98+LrzwQkBLFUVERPQ+f/311wNw3nnn0dDQQF1dHTNnzmTp0qXccMMNXHXVVURHRx9n95tvvumTH3fffTd33303b7/9Nk888QSvv/66jk/VPwwfATFwHpYQgmBfL90prEBz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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "# create structures of 2 atoms, and plot the force on the second atom\n", - "cell = np.eye(3) * 1000\n", - "seps = np.arange(2, 5, 0.01)\n", - "specs = [[47, 47], [47, 53], [53, 53]]\n", - "store_frcs = np.zeros((3, len(seps)))\n", - "for m, sep in enumerate(seps):\n", - " pos = np.array([[0, 0, 0], [sep, 0, 0]])\n", - " for n, spec_curr in enumerate(specs):\n", - " struc_curr = struc.Structure(cell, spec_curr, pos)\n", - " frcs = \\\n", - " lj_module.run_dft_par(structure=struc_curr, dft_kwargs=lj_params)\n", - " store_frcs[n, m] = frcs[1, 0]\n", - "\n", - "plt.plot(seps, store_frcs[0], label='sig = 2.0, eps = 3.0')\n", - "plt.plot(seps, store_frcs[1], label='sig = 1.5, eps = 1.8')\n", - "plt.plot(seps, store_frcs[2], label='sig = 1.0, eps = 1.0')\n", - "plt.xlim(2, 4)\n", - "plt.ylim(-4, 4)\n", - "plt.xlabel('separation ($\\AA$)')\n", - "plt.ylabel('force (eV/$\\AA$)')\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "rzOBo-6tfn1y" - }, - "source": [ - "#### Perform an on-the-fly training simulation." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "7H3ShRCVpBWN" - }, - "source": [ - "#### Step 1: Set up the initial structure." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "1lnkDIvmhEQQ" - }, - "source": [ - " Let's create a bcc (body centered cubic) Lennard Jones crystal." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "id": "391yPEiYrD70" - }, - "outputs": [], - "source": [ - "# create bcc unit cell\n", - "alat = 2.54\n", - "unit_cell = np.eye(3) * alat\n", - "\n", - "# define bcc positions\n", - "unit_positions = np.array([[0, 0, 0],\n", - " [1/2, 1/2, 1/2]]) * alat\n", - "\n", - "# make a supercell\n", - "sc_size = 3\n", - "positions = md_helper.get_supercell_positions(sc_size, unit_cell, unit_positions)\n", - "cell = unit_cell * sc_size\n", - "\n", - "# jitter positions to give nonzero force on first frame\n", - "for atom_pos in positions:\n", - " for coord in range(3):\n", - " atom_pos[coord] += (2*np.random.random()-1) * 0.05\n", - "\n", - "# create initial structure\n", - "species = ['Ag', 'I'] * sc_size ** 3\n", - "struc_curr = struc.Structure(cell, species, positions)\n", - "\n", - "# create pseudo input file\n", - "mass_dictionary = {'Ag': 108, 'I': 127}\n", - "input_dictionary = {'positions': positions, 'species': species, 'cell': cell,\n", - " 'masses': mass_dictionary}\n", - "input_file_name = 'lj.in'\n", - "with open(input_file_name, 'wb') as input_file:\n", - " pickle.dump(input_dictionary, input_file)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 617 - }, - "id": "WOXg1lAXA9tk", - "outputId": "af08801f-3826-4cb5-bf93-eeef06b0adbf" - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - "\n", - " ASE atomic visualization\n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "# view the structure \n", - "view(struc_curr.to_ase_atoms(), viewer='x3d')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "oedgfWfdgeHk" - }, - "source": [ - "#### Step 2: Set up a GP model." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "id": "eX8P6XE6ghjl" - }, - "outputs": [], - "source": [ - "# create gaussian process model\n", - "kernels = ['twobody']\n", - "component = 'mc'\n", - "hyps = np.array([0.1, 1., 0.06])\n", - "hyp_labels = ['Sigma', 'Length Scale', 'Noise']\n", - "cutoffs = {'twobody': 5.0}\n", - "maxiter = 50\n", - "\n", - "gp_model = gp.GaussianProcess(\n", - " kernels=kernels,\n", - " component=component,\n", - " hyps=hyps,\n", - " hyp_labels=hyp_labels,\n", - " cutoffs=cutoffs,\n", - " maxiter=50\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8HZNHX5qgua5" - }, - "source": [ - "#### Step 3: Set up an OTF training object." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "id": "8zTpBIBmg1NF" - }, - "outputs": [], - "source": [ - "# create otf object\n", - "dt = 0.001 # time step (ps)\n", - "number_of_steps = 50\n", - "dft_loc = None # path to dft executable would usually go here\n", - "std_tolerance_factor = -0.01 # 10 meV/A\n", - "init_atoms = [0, 25, 50] # initial atoms added to the training set\n", - "max_atoms_added = 5 # number of atoms added when dft is called\n", - "freeze_hyps = 5 # no hyperparameter optimization after this many updates\n", - "\n", - "# rescale the temperature halfway through the simulation\n", - "rescale_steps = [10] # rescale at step 10\n", - "rescale_temps = [1000]\n", - " \n", - "otf_model = otf.OTF(\n", - " # MD arguments\n", - " dt,\n", - " number_of_steps,\n", - " rescale_steps=rescale_steps,\n", - " rescale_temps=rescale_temps,\n", - " # FLARE arguments\n", - " gp=gp_model,\n", - " # OTF arguments\n", - " std_tolerance_factor=std_tolerance_factor,\n", - " init_atoms=init_atoms,\n", - " max_atoms_added=max_atoms_added,\n", - " freeze_hyps=freeze_hyps,\n", - " # DFT arguments\n", - " force_source=lj_module,\n", - " dft_loc=dft_loc,\n", - " dft_input=input_file_name,\n", - " dft_kwargs=lj_params\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "AfRnWJz6isCF" - }, - "source": [ - "#### Step 4: Perform the simulation!" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "id": "WeJwD-rgn541" - }, - "outputs": [], - "source": [ - "# perform flare run (this will take about 2.5 minutes)\n", - "otf_model.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "S4vU6y9hfwng" - }, - "source": [ - "#### Parsing the OTF output file." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 279 - }, - "id": "Elwp3GytoA5h", - "outputId": "3607fa29-d213-48a4-847b-b51e04d1e939" - }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAEGCAYAAACZ0MnKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nO3dd3hUZfbA8e9JAoTeewtVuigE7BpAxQaiuxErVpRVV9dVRF3FsrqK7WdHRBQVgYgF7CKMCEgJRXoLvRM6CaSf3x9zgxEpQzIzd8r5PM88zL1zZ+65BObkLfe8oqoYY4wxvohxOwBjjDHhw5KGMcYYn1nSMMYY4zNLGsYYY3xmScMYY4zP4twOIBBq1KihCQkJbodhjDFhZe7cuTtVtebxjonIpJGQkMCcOXPcDsMYY8KKiKw/0THWPWWMMcZnljSMMcb4LGBJQ0RGiMgOEVlcZN+LIrJcRBaKyJciUqXIa4+ISJqIrBCRi4vs7+nsSxORQYGK1xhjzIkFsqXxIdDziH0TgXaq2gFYCTwCICJtgL5AW+c9b4tIrIjEAm8BlwBtgGudY40xxrggYElDVX8Fdh+x7ydVzXM2ZwINnOe9gTGqmq2qa4E0oIvzSFPVNaqaA4xxjjXGGOMCN8c0bgW+d57XBzYWeW2Ts+9Y+/9CRPqLyBwRmZOenh6AcE2gDBkyBI/H86d9Ho+HIUOGuBSRMeZYXEkaIvIYkAeM8tdnquowVe2sqp1r1jzuNGMTYhITE0lOTj6cODweD8nJySQmJrocmTHmSEG/T0NEbgYuB7rrH3XZNwMNixzWwNnHcfabCJCXX0CjdokMeuNTrn/mA7r9vJCfPn2HlJQUkpKS3A7PGHMECeR6GiKSAHyjqu2c7Z7AK8D5qppe5Li2wKd4xzDqAZOAFoDgHTDvjjdZpALXqeqS4523c+fOajf3haat+w4xNnUjK7cfYNX2DNbtyiQ3/6//ButVjqdzQjUSm1SjS0I1WtSqQEyMuBCxMdFDROaqaufjHROwloaIjAYuAGqIyCZgMN7ZUmWAiSICMFNV71LVJSKSAizF2211t6rmO59zD/AjEAuMOFHCMKEpMzuPd6esZtjUNeTkFdCoWjma16pI99a1aV6rAvs2LGfQPbdxzS0D+PzX36l/1c3MXLOLCQu2ANC4ejme6tWWC06p5fKVGBPdAtrScIu1NEJHfoHy+bxNvPTjCnYcyObyDnV5uGcrGlYrd/iYwjGMwi6pwu2xY8fS/NQzmLV2F0OnrGZ1eiaXdajLE5e3oXaleBevypjI5GpLw5gZq3fxzDdLWbp1Px0bVuGdGzrRqXHVvxyXmpr6pzGMpKQkUlJSSE1NpVu3bjSqXo5eHesxbMoa3vCkMWVFOg9e1JIbz0wg1rqsjAkqa2kYv8svUP7z1SJGz95I/SplGdjzFHqdWg+nS7JE1u3M5PHxi5m6aicdGlTmuT7taVe/sh+iNsb40tKw2lPGr3LzC7hvzHxGz97Inec1ZdK/z6d3x/p+SRgACTXK89GtXXj92tPYsjeLXm9OY9zcTX75bGPMiVn3lPGb7Lx87v10Pj8t3c6gS1px1/nNAnIeEaHXqfU4v2VN/jFqLg+NW0BBgZKc2PDEbzbGlIi1NIxfZOXm0/+jufy0dDtP9WobsIRRVOWypXi/XyLntqjJwM8XMmrWCZcCMMaUkCUNU2KZ2Xnc8kEqv65K5/mr2tPvrISgnTu+VCzDbuxEt1a1eOzLxXw0Y13Qzm1MNLKkYUpkf1Yu/UbMZtbaXbySfCp9uzQKegzxpWIZekMnLmxTmyfGL2H41DVBj8GYaGFJwxRbVm4+N74/m9837uXN606nz2kNTvymACkdF8Pb15/Ope3r8N9vlzF0ymrXYjEmktlAuCm2/367lAUb9zL0htPp2a6u2+FQKjaG1/ueRmzMAp7/fjmlY2O49ZwmbodlTESxpGGK5duFW/lk5gbuPK9pSCSMQnGxMfzfNR3Jzs3n2e+W0b5BZRITqrkdljERw7qnzEnbsOsggz5fyGmNqvDgxae4Hc5fxMYILyWfSsOqZbnn03nszMh2OyRjIoYlDXNScvIKuGf0PETg9b6nUSo2NP8JVYovxdvXd2LvwVzuGzOf/ILIq3xgjBtC83+8CVkv/LCchZv2MeRvp/6p6GAoalOvEs/0bsf0tF28NmmV2+EYExEsaRif/bx0O+9PW8vNZyXQs10dt8PxSXJiQ/7WqQFvTF7FlJW2DLAxJWVJw/hky95DPDhuAe3qV+KRS1u5Hc5JeaZ3O06pXZH7x8xny95DbodjTFizpGFOKC+/gH+Onk9evvLmtadTJi7W7ZBOStnSsbx9/enk5it3fzqPnLwCt0MyJmxZ0jAn9P60tcxZv4dn+7QjoUZ5t8MplqY1K/DC1R2Yv2Evz3+/3O1wjAlbljTMce3Yn8Xrk1bRo3Utenes73Y4JXJZh7rcdGZjRkxfy9z1u90Ox5iwZEnDHNfzPywnN1/5z2Vt3A7FLx7u2Yp6leN59IvF5OZbN5UxJ8uShjmmeRv28MW8zdx+bpOw7ZY6UvkycTzdux0rth/gPStsaMxJs6RhjqqgQHlywhJqVyrD3UnN3Q7Hr3q0qU3PtnV47edVbNh10O1wjAkrljTMUY2bu4mFm/bxyCWtKV8m8kqUPdmrLaViY/jP+MWo2t3ixvjKkob5i32Hcnnhh+V0alyV3h3ruR1OQNSpHM+DF7Xk15XpfL1wq9vhGBM2LGmYv3h90ip2H8zhqV5tERG3wwmYG89MoEODyjz99VL2Hcx1OxxjwkLAkoaIjBCRHSKyuMi+aiIyUURWOX9WdfaLiLwuImkislBETi/ynn7O8atEpF+g4jVeq7YfYORv6+ib2Ih29Su7HU5AxcYIp+yfx66MbF748Y97NzweD0OGDHExMmNCVyBbGh8CPY/YNwiYpKotgEnONsAlQAvn0R94B7xJBhgMdAW6AIMLE43xP1Xlqa+XUq50LA9e1NLtcILi0jPbk7PoBz6dtYG563fj8XhITk4mMTHR7dCMCUkBSxqq+itw5B1UvYGRzvORwJVF9n+kXjOBKiJSF7gYmKiqu1V1DzCRvyYi4yc/Ld3OtLSdPHBhS6pXKON2OEGRlJTE8Pv7UJCxi9uGTiK577WkpKSQlJTkdmjGhKRgj2nUVtXCUcdtQG3neX1gY5HjNjn7jrX/L0Skv4jMEZE56elWzbQ4vpy3mXqV47nhjMZuhxJUl1zYje5VdrJXy3HB7Y9bwjDmOFwbCFfvPEe/zXVU1WGq2llVO9esWdNfHxtV9mflUq9KWeJCdGGlQPF4PHzz7v+oI/uYnVGN7yZOdjskY0JWsL8dtjvdTjh/7nD2bwYaFjmugbPvWPtNAGRm50XkPRnHUziGkZKSwrC7L0XKVuL2l8bg8XjcDs2YkBTspDEBKJwB1Q8YX2T/Tc4sqjOAfU431o/ARSJS1RkAv8jZZwIgIzuPClGWNFJTUw+PYXRoUIXLO9SlQufe/DJrvtuhGROSAvYNISKjgQuAGiKyCe8sqOeBFBG5DVgPJDuHfwdcCqQBB4FbAFR1t4g8A6Q6xz2tqlaeNEAysvMoXya81sooqYEDB/5p+6GLT+GHxdvIPeVClyIyJrQFLGmo6rXHeKn7UY5V4O5jfM4IYIQfQzPHkJmdT4UypdwOw1WNq5fn+q6N+GTWBm47pwnNalZwOyRjQkp0jXiaY1JVMnPyqBBlLY2jubd7C+LjYnjxhxVuh2JMyLGkYQA4mJOPKlE3EH40NSqU4c7zm/HDkm3MXb/H7XCMCSmWNAzgnTkFljQK3X5uE2pUKMPz3y+zKrjGFGFJwwBwwEka0TZ76ljKlY7j/h4tSF23h0nLdpz4DcZECUsaBvijpWFJ4w/XJDakaY3yvPDDcvJsaVhjAEsaxpFh3VN/USo2hoE9T2HVjgw+n7fJ7XCMCQmWNAzgnW4L1tI40sVt63Bqwyq8MTmNXGttGGNJw3j9MRBuU26LEhHu696cTXsO8eV8q2BjjCUNA9hA+PEknVKLdvUr8ZYnzcY2TNSzpGGAIgPh8ZY0jiQi/LNbC9bvOsjXC7e4HY4xrrKkYQBv0ogRKFvKuqeO5sI2tWlVpyJvTE4jv8Du2zDRy5KGAZxihaXjEBG3QwlJIsI/u7dgTXom3y7aeuI3GBOhLGkYADKyom8tjZPVs20dWtSqwBuTVlFgrQ0TpSxpGAAyc6KvLPrJiokR7u3eglU7MvhhyTa3wzHGFZY0DAAZ2flUiI/usui+uKx9XZrWLM/r1towUcqShgG8A+FWFv3EYmOEe7s1Z/m2A/y8bLvb4RgTdJY0DOCsD17axjR8cUWHejSuXo7XJ6+yCrgmpOzJzGHZ1v0BPYclDQPAgazoWx+8uOJiY7g7qTmLN+/Hs8Iq4Br35eUX8NGMdVzw0i/8c/T8gP4yY0nDAIUD4ZY0fNXntPo0qFqW1yalWWvDuGrmml1c/sY0nhi/hLb1KvHmdacHdOq8JQ0DOGMadje4z0rFxjDggmYs2LiXmWt2ux2OiUJb9h7ink/n0XfYTA5k5fHO9acz6vaunFKnYkDPa98Shuy8fHLz1bqnTtLVpzfg1YkreffX1ZzZrLrb4ZgokV+gDJ2ymjcmr0IV7u/RgjvPa0bZ0sGZyGLfEuZwWfTyQfpHFyniS8Vyy9lNePHHFSzbup/WdSu5HZKJcPuzcvnn6Pn8siKdS9rV4bHLWtOgarmgxmDdU4aMLFuAqbhu6NqYcqVjGfbrGrdDMRFu7c5M+rw1nWmrdvJsn3a8c0OnoCcMsKRh+GPVvoo2pnHSKpcrxbVdGjFhwRY27TnodjgmQk1P28mVb01nd2YOH9/Wleu7NnYtFksahswca2mUxG3nNEGAEdPWuR2KiTCqysjf1nHTiNnUrlSG8Xef4/r4mc9JQ0TKi4hfOr1F5F8iskREFovIaBGJF5EmIjJLRNJEZKyIlHaOLeNspzmvJ/gjBvMHWx+8ZOpVKUuvjvUYk7qBvQdz3A7HRIicvAIe/XIxgycsIemUmnw+4CwaVQ9+d9SRjpk0RCRGRK4TkW9FZAewHNgqIktF5EURaV6cE4pIfeCfQGdVbQfEAn2BF4BXVbU5sAe4zXnLbcAeZ/+rznHGjzJt1b4S639eUw7m5PPxjPVuh2IiQF5+AfeNmc/o2RsYcEEz3r2xMxVDpDbc8VoaHqAZ8AhQR1Ubqmot4BxgJvCCiNxQzPPGAWVFJA4oB2wFugHjnNdHAlc6z3s72zivdxdb9MGvbCC85FrVqUTSKTX58Ld1ZOXmux2OCWMFBcpD4xby/eJtPH55Gx7u2YrYmND5yjte0uihqs+o6kJVPbwwsqruBr5W1auBsSd7QlXdDLwEbMCbLPYBc4G9qprnHLYJqO88rw9sdN6b5xz/l049EekvInNEZE56evrJhhXVMqyl4Rd3nt+MXZk5jJu7ye1QTJhSVR77ajFfzt/Mgxe15LZzmrgd0l8cL2k8crSdIlIZ+AlAVXNP9oQiUhVv66EJUA8oD/Q82c85kqoOU9XOqtq5Zs2aJf24qGL3afhH1ybV6NiwCu9NXWNLwpqTpqo8880yRs/ewD8uaMY93Vq4HdJRHS9pnCMizxbdISK1gSl4u66KqwewVlXTnaTzBXA2UMXprgJoAGx2nm8GGjrnjwMqA7tKcH5zhMycPOJLxRAXa5PpSkJEqL1nIet3HeSHxX8s0uTxeBgyZIiLkZlw8MrElYyYvpabz0rgoYtPcTucYzret0Qv4FQReQVARFoA04GhqvpUCc65AThDRMo5YxPdgaV4E9HfnGP6AeOd5xOcbZzXJ6tViPMrq3DrP8lnnULBvm28+M3vqCoej4fk5GQSExPdDs2EsLd/SeONyWn0TWzI4CvaBLTgYEkdM2moahbQB0gQkdHAz8BDqjq0JCdU1Vl4B7TnAYucGIYBDwMPiEga3jGL9523vA9Ud/Y/AAwqyfnNX2VmW4Vbf+nerRu3ntmIdfsLuPPxl0lOTiYlJYWkpCS3QzMh6qMZ6xjywwp6d6zHs33ah3TCgOPUnhKRB5yns4CBwFSgSeF+VX2luCdV1cHA4CN2rwG6HOXYLODvxT2XOTHvqn2WNPxl0LXdGPP413y1/AADBgywhGGOadqqnTw5YQk9Wtfmpb+fGlKzpI7leN1TFZ1HPPA6ML/IvsDW3jVBlWEtDb+aMe1X9s/7hnLNuzD006/weEoyBGgi1YZdB7ln9Dxa1KrIa307UipMxhSP+U1RwnELE0Yyc/KoVTHe7TAiQuEYxvBPxjJwajbn3/+idVGZvziYk0f/j+dQUKAMu6lTWP3Sdrw7wv/jTI891uvdROTywIRlgikjy1oa/pKamkpKSgq9L+7GVac3YOa2AoZ/MpbU1FS3QzMhQlV56LOFrNx+gDevO53G1cu7HdJJOd43xSLgGxHJwjtonY63q6oF0BHvwPhzAY/QBFxGdj4Vytg9Gv4wcODAw89vO6cJo2dvYG1sAwYO7OZiVCaUvP3Lar5dtJVHL23FeS3D756y482eGq+qZwN3AUvw1ojaD3wCdFHVf6mq3XodAWwgPDCa16pAt1a1+HjGeistYgDwLN/BSz+toNep9bjj3KZuh1MsJ/ymUNVVwKogxGJckF+gHMrNt+6pALn93CZc994svpq/mb5dGrkdjnHRmvQM/jlmPq3rVOKFqzuE/NTaYwmP4XoTMIVraVhLIzDObFqdNnUrMXzaWgqstEjUyszOo//HcykVG8OwmzoFbT3vQLCkEeWswm1giQh3nNeEtB0ZTFlpvbnR6plvlrI6PYM3rz3NlSVa/cmSRpSztTQC77L29ahTKZ73pto64tHoh8VbGZO6kQHnN+Os5jXcDqfETpg0RKSliEwSkcXOdgcR+U/gQzPBYGXRA690XAz9zkrgt9W7WLJln9vhmCDati+LQV8sokODytzfo6Xb4fiFLy2N9/CWSc8FUNWFeFfaMxHgcFl0SxoBdV2XRpQrHcv7U9e6HYoJkoIC5d+f/U52bgH/d01HSsdFRseOL1dRTlVnH7Ev76hHmrDzx/rg4TswFw4qlytFcueGTFiwhW37stwOxwTB8GlrmJ62i8FXtKFpzQpuh+M3viSNnSLSDFAAEfkb3hX3TASw7qngufXsJhSo8uFv69wOxQTY4s37ePHHFfRsW4drEhu6HY5f+ZI07gbeBVqJyGbgfrw3/JkIYAPhwdOoejkubluHT2etP/z3biLPoZx87hszn2rlS/O/q0K/1PnJOm7SEJFY4B+q2gOoCbRS1XNUdX1QojMB90f3lCWNYLj93Kbsz8rjszkb3Q7FBMh/v13K6vRMXknuSNXypd0Ox++OmzRUNR84x3meqaoHghKVCZrM7DziYoQyETJIF+o6Na7K6Y2qMGL6OltHPAJNWradUbM20P+8ppwdAdNrj8aXb4r5IjJBRG4UkasKHwGPzARF4VoakdaEDmV3nNuUDbsP8tOSbSc+2ISNfYdyeeSLRbSqU5F/XxQZ02uPxpc+iXhgF1C0TKcCXwQkIhNUGVasMOgualuHhtXK8t7UNVzSvq7b4Rg/ee7bZezKzOH9fomUiYvc2Yi+FCy8JRiBGHdYhdvgi40Rbj27CU99vZS56/fQqfExl60xYWLqqnTGztnIXec3o32Dym6HE1An/LYQkQ9wptsWpaq3BiQiE1SZ2fl2j4YLkjs35NWJK3l/2ho6Ne7kdjimBDKz8xj0+SKa1izP/T1auB1OwPnyK+Y3RZ7HA32ALYEJxwRbRnYeFeOtpRFs5cvEcV3Xxgz7dTUbdx+kYbXwLmIXzV78cQVb9h3iszvPJL5U5P8CdsKBcFX9vMhjFJAMdA58aCYYbEzDPTeflUCMCO9Ps9Ii4Sp13W4+/G0d/c5MoHNCNbfDCYrizLNsAdTydyDGHZnZtj64W+pUjueKU+uRMmcj+w7muh2OOUlZufk8PG4hDaqW5aGLT3E7nKDxpcrtARHZX/gAvgYeDnxoJhispeGu289twsGcfEanbnA7FHOSXv15JWt2ZvLC1R2i6hcvX7qnKqpqpSKPlqr6eUlOKiJVRGSciCwXkWUicqaIVBORiSKyyvmzqnOsiMjrIpImIgtF5PSSnNv8QVVt9pTLvv3kXVpXi+HD6evIySsAwOPxMGTIEJcjM8ezcNNe3vt1DX0TG0bsTXzH4ktLY5Iv+07Sa8APqtoKOBVYBgwCJqlqC2CSsw1wCd4usRZAf+CdEp7bOLJyCyhQKyHipsTEROaPfolt+7P4dtEWPB4PycnJJCYmuh2aOYbc/AIGjltIrYrxPHpZa7fDCbpjfluISDxQDqjh/NZfeMtwJaB+cU8oIpWB84CbAVQ1B8gRkd7ABc5hI4Ff8HaD9QY+UlUFZjqtlLqqapV2S+hAtrcfvYJNuXVNUlISo15WbhmbxtMpmWwafjcpKSkkJSW5HZo5hhHT1rJ82wHevbETleJLuR1O0B2vpXEnMBdo5fxZ+BgPvFmCczYB0oEPRGS+iAwXkfJA7SKJYBtQ23leHyha3W0TJUha5g+2AFNo6N6tG2fXyGaPlqf3nQ9bwghhm/Yc5P9+XkWP1rW5uG0dt8NxxTGThqq+pqpNgAdVtamqNnEep6pqSZJGHHA68I6qngZk8kdXVOG5laPcUHg8ItJfROaIyJz09PQShBc9rCx6aPB4PPw8/DnKksOP6/PxeDxuh2SOQlV5YvwSROCp3m3dDsc1vgyEvyEi7UQkWURuKnyU4JybgE2qOsvZHoc3iWwXkboAzp87nNc3A0VXMWng7DsyzmGq2llVO9esWbME4UUPW4DJfYVjGCljPuW+SzoQW78dfQc8ZIkjBP2weBuTl+/ggQtbUr9KWbfDcY0vA+GDgTecRxIwBOhV3BOq6jZgo4gUTmzuDiwFJgD9nH398HaD4ey/yZlFdQawz8Yz/CPT1tJwXWpq6uExjOu6NqJimTjOv+u/pKamuh2aKeJAVi5Pfr2ENnUrcfNZCW6H4ypfvi3+hneG03xVvUVEagOflPC89wKjRKQ0sAa4BW8CSxGR24D1eO88B/gOuBRIAw46xxo/sAWY3Ddw4MDDzyvFl+K6Mxrx3q9reOnBe1yMyhzp5Z9WsuNANu/e2Jm42Ohee8aXb4tDqlogInkiUglvt1GJFr1V1d85eimS7kc5VvEuOWv8rDBpWO2p0HHr2U34YNo63pu6hmeubOd2OAZYsHEvI2es46YzGtOxYRW3w3GdLylzjohUAd7DO3tqHjAjoFGZoLDuqdBTu1I8fU6rT8qcjezKyHY7nKiXl1/Ao18uomaFMvw7ikqFHM+J1ggX4H+quldVhwIXAv1sjY3IkOFMuS0XBZU5w8kd5zUlJ7+Akb+tczuUqDdyxnqWbNnP4CvaRuU9GUdzojXCFe+YQuH2OlVdGPCoTFBkZudRvnQsMTG21GsoaV6rAhe2rs3IGesPtwZN8G3dd4hXflpB0ik1ubR9dN6TcTS+dE/NExGraRCBMrKswm2ouvP8Zuw7lEvKnI0nPtgExFMTlpKvytO92+HtdDHgW9LoCswQkdVOwcBFImKtjQiQkZNHBRsED0mdGlelS0I1hk9dS25+gdvhRB3P8h38sGQb93ZrYQtkHcGXpHEx0AzoBlwBXO78acKcVbgNbTV2zmfz3kN8u/CP25KsAm7gHcrJ54kJi2lWszx3nNvU7XBCji93hK/HO8W2m/P8oC/vM6HPO6ZhSSNUXX1WKwr2bOLlbxegqlYBN0je8qSxcfch/ntle0rH2VfdkXy9I/xh4BFnVylKfnOfCQEHbEwjpHXv1o07zm3Kxgzltsdf9ZYbsQq4AZW2I4N3f13NVafV58xm1d0OJyT5kkb74C0bkgmgqluAioEMygRHZk6elUUPcYOu7U4Fsvh+g3LXXQMsYQSQqvL4V4spWyo2KtfJ8JUvSSOnaNVZp4y5iQCZ2fk2EB7ipv06hV3TRlOmXive+3qqFTIMoPG/b2HGml0M7NmKGhXKuB1OyPIlaaSIyLtAFRG5A/gZ793hJsxlZFv3VCgrHMP46Kl/UL9KWdr2HUhycrIljgDYdyiX/367lFMbVuG6Lo3cDiek+TIQ/hLe8uWfAy2BJ1T1jUAHZgIrJ6+AnLwCKthAeMgqrIB7YfduDLigGav3FjD47dFWATcAXvpxBbszc3j2ynZ2s+sJ+PqNsQgoi7eLalHgwjHBYnWnQl/RCrh/79yAtzxp/LKzLJ899JCLUUWeBRv38sms9fQ7M4F29Su7HU7I82X21O3AbOAqvGXSZ4rIrYEOzASWLcAUXsrExTLggmbMWb+HGat3uR1OxPhTQcKLWrodTljwZUzjIeA0Vb1ZVfsBnfBOwTVhLDPHSRo2EB42kjs3pHalMvzfpFVuhxIxPvxtHUu27OfJXm2paAUJfeJL0tgFHCiyfcDZZ8KYdU+Fn/hSsQw4vxmz1+621oYfbN57iFcmrqRbq1pc0s4KEvrKl6SRBswSkSedG/1mAitF5AEReSCw4ZlAKSyLbvdphJe+XRpRs2IZXpu00u1QwpqqMnj8YlTh6d5trSDhSfAlaawGvsK5TwPv2t1r8d7gZzf5hamMLGtphKP4UrG0zF/HzDW7mbXmj9aG1aQ6OT8u2c7Py3bwwIUtaVDVChKejBN+Y6jqU8EIxATX4e4pm3Ibdm48qylTJ2zjqXGz+W7gJYfv50hJSXE7tLBwICuXJycsoXXdStxydoLb4YQdX2ZPdRaRL0VknlMafaGVRg9/tj54+OrZoxvXnlaDpbsLuOs/Q6wm1Ul6+aeVbD+Qxf+uak9crBUkPFm+/I2NAj4ArsZbEr3wYcKYDYSHt8HXdyOeXL5aU2A1qU7Cgo17GTljHTed0ZiODau4HU5Y8iVppKvqBFVdq6rrCx8Bj8wEVEZOHqXjYihlv2mFpVnTp7LvtzHEN2rPe9/OsNIiPsjLL+CRLxZRq2IZ/n3xKW6HE7Z8+cYYLCLDReRaEbmq8BHwyExAZWTZAkzh6nBNqsH9aVqjPE36/Ivka/pa4jiBD39bx9Kt+3nyirZUsnsyis2Xb41bgFZ417jQTToAABfDSURBVNEoXHdSgS8CFZQJPFu1L3wV1qRKSkoiv/Y27vpkLgNe+pjU1FTrpjqGdTszeemnFXRvVYuedk9GifjyrZGoqtaWizAZ2fk2nhGmitakurhtbRITqvLdhoP88pDdNnU0BQXKwM8XUio2hmf7tLd7MkrIl+6p30Skjb9PLCKxIjJfRL5xtpuIyCwRSRORsSJS2tlfxtlOc15P8Hcs0cjb0rAb+8KdiPDopa3ZmZHNsCmr3Q4nJH08cz2z1+7m8cvbUKdyvNvhhD1fksYZwO8issKZbrvIT1Nu7wOWFdl+AXhVVZsDe4DbnP23AXuc/a86x5kSsrU0IsdpjapyWYe6vDd1Ldv3Z7kdTkjZsOsgz3+/nPNa1uTvnRq4HU5E8CVp9ARaABfhnWp7OSWccisiDYDLgOHOtgDd8K7bATASuNJ53tvZxnm9u1j7ssQyLWlElIcvbkVeQQGv/GTlRQp5u6UWEBsjPH+VdUv5iy+LMK0HGgLdnOcHfXnfCfwfMJA/BtarA3tVNc/Z3gTUd57XBzY6seQB+5zj/0RE+ovIHBGZk56eXsLwIl9Gdh4VLWlEjEbVy3HTmQl8NncjK7YdOPEbosCo2RuYuWY3/7msNfWqlHU7nIjhyx3hg/GWQn/E2VUK+KS4JxSRy4Edqjq3uJ9xNKo6TFU7q2rnmjVr+vOjI5K1NCKPLv6e+Fj43/d/9PpGa02qjbsP8r/vlnFuixpck9jQ7XAiii8thj5ALyATQFW3ULJChWcDvURkHTAGb7fUa3jXIC/8FmsAbHaeb8bb0sF5vTJWmr1ECgqUzBybPRVpzu16Onumj+GXFelMW7Xz8P0ciYmJbocWVKrKoC8WIsDzV3ewbik/8yVp5Kiq4lS5FZHyJTmhqj6iqg1UNQHoC0xW1esBD96VAQH64a2mCzDB2cZ5fbITjymmwwsw2eypiJKUlMTIx26h4MBO7h0xmeRr+kZlTarRszcyPW0Xj17WmvrWLeV3viSNFBF5F29L4A7gZ5wBbD97GHhARNLwjlm87+x/H6ju7H8AGBSAc0eVTGctDWtpRJ6LenQjqeoe9mh5zrtjcNQljA27DvLcd8s4q1l1ruvSyO1wIpIvA+Ev4Z219DlwCvCEqr7uj5Or6i+qernzfI2qdlHV5qr6d1XNdvZnOdvNndfX+OPc0czWB49cHo+H7959lgayhznZdRjzzSS3QwqanLwC7h09DxF4wbqlAsaXgfAXVHWiqj6kqg+q6kQRsXslwlimJY2IVHRdjXGD/ka5+DI8lDKPyZMnux1aULz00woWbNrHkKs70LCaLawUKL50T114lH2X+DsQEzxWFj0yFa1JVadyPI9f0Z7Yem34aFqa26EFnGfFDob9uoYbzmjEJe3ruh1ORDvmt4aIDAD+ATQ94g7wisD0QAdmAueAtTQiUtGaVAB9Exsy/vfNzN0ax/b9WdSuFJklNLbty+LfKQtoVaci/7nM7xWPzBGO19L4FO+d3xP48+JLnVT1hiDEZgLEWhrRISZGeP7qDuTkFfDE+MVuhxMQ+QXK/WPncygnnzevO534UjYjMNCOmTRUdZ+qrlPVa4suvqSqu4MZoPE/G9OIHk1qlOdfF7bkxyXb+X7RVrfD8bs3J6cxc81unu7dlua1KrgdTlSwZduiUIYz5daSRnS4/ZwmtKtficfHL2HvwRy3w/GbmWt28dqklfQ5rT5/s2KEQWNJIwplZucRIxBfyn780SAuNoYXru7AnoM5PPvtshO/IQzszszhvjHzaVy9PM9c2c6m1waRfWtEocKy6PYfLXq0rVeZ/uc15bO5m/h56Xa3wymR7Lx8Bnwylz2Zubxx7WnWYg4ySxpRKMOWeo1K93VvQbv6lbh/7O+k7chwO5xiKShQBo5byKy1u3nx7x1oV7+y2yFFHUsaUcjWB49O8aVieffGzpSJi6H/R3PYdyjX7ZBO2ssTVzD+9y08dPEp9O5Y/8RvMH5nSSMK2ap90at+lbK8c0MnNuw+yP1j5pNfED61P0fP3sBbntX0TWzIPy5o5nY4UcuSRhSy7qno1qVJNQb3aotnRTov/7TC7XB88suKHfznq8Wc17KmDXy7zJJGFPIuwGQ3QUWzG7o24touDXn7l9V8s3CL2+Ec15It+7h71Dxa1q7I29efTqlY+9pyk/3tR6HM7HwqlCnldhjGRSLCU73aUScukwfGzmfJln2HXwul1f627D3ErR+mUqlsKT64OdFayCHAkkYU8nZPWUsj2pWOi2HQOTXI2r+bm4ZNZ1dGdkit9rdx90FueH8Wmdn5jLg5kTqVI7N2VrixtB1lVNXWBzeHXdmzG7uzfuapaRlc+Ox4Nn7ySEis9rd48z5u+TCV7Fxvwmhdt5Kr8Zg/WEsjiqgqT3+zlLwCpWXtkizzbiLJrVf2oHv8anZmx1L7+iE0buduK+PXlelc8+4MSsUI4wacRZcm1VyNx/yZJY0o8srElXwwfR23nt2E3h3ruR2OCREej4ev332ey8ulceBQDr1en8K8DXuCcu4hQ4bg8XgOb4+bu4lbPphNfH4GX959tv1yE4IsaUSJt39J443JafRNbMjjl7e2KYsG+PNqf2//92FeuLAW+3dtp+/Q34JSbiQxMZHk5GQmT57MG5NW8eBnC8jZtJhnLqgWset/hDtLGlHgw+lrGfLDCnp3rMezfdpbwjCHFV3tD6Dv5d15o1cCleUQ/T+ew6hZ6wN6/qSkJD4ePZYbX/+elyeuJHfVdEbcnMilF3YL6HlN8Ylq+NwR6qvOnTvrnDlz3A4jJKSkbmTg5wu5qE1t3rI57sZHmdl53PPpPDwr0rnr/Gbc36OF3xc4UlW+XriV579bxpZ9WeybkcI95zXmmWee9ut5jO9EZK6qdj7eMfYNEsEmLNjCw18s5LyWNXnjutMsYRiflS8Tx3s3debaLg0ZOmU15w3x8NGMdWTn5fvl8xdu2svfh87gn6PnE1eQzaFvn+Pe8xszdOg7fxrjMKHHWhoRauLS7dz1yVw6Na7KyFu6ULa03Zdhimfmml28/NMKUtftoX6VsrTITeOGs5rTo/sfXUgej4fU1NS/rFN+pB37sxjy4wo+n7eJ6uVLc0VjePPf15MydixJSUl/GmNxe9pvNLKWRpSauiqdu0fNo139yrzfr7MlDFMiZzStTsqdZ/LRrV2oUaE0v2TW59Zxa3l21ER27M9i8uTJx7whsKBAWb5tP5/MXM+/xv5O0ku/MP73zfQ/tymeBy+g7Nb5hxMGeMc4UlJSSE1NDfZlGh9ZSyPCzF67m5tGzCKhennG9D+DKuVKux2SiSCqys/LdvD0F3PZmOH97tDcLBpVLUvbxrVpXKMcjauVZ1dGNnPW72Hehj0cyPKuSV+jQmnOaV6D+3u0JKFGeTcvwxyDLy2NoN8WLCINgY+A2oACw1T1NRGpBowFEoB1QLKq7hHvVJ/XgEuBg8DNqjov2HGHg4Wb9nLrh6nUq1KWj2/ragnD+J2IcGGb2nRvdQl3PvEyY777hXN6Xkm9Bo1YteMAk5fvICe/AICWtStweYd6dG5clc4JVWlUrZzN3IsAbtSSyAP+rarzRKQiMFdEJgI3A5NU9XkRGQQMAh4GLgFaOI+uwDvOn6aI5dv2c9OI2VQpV4pRt3elZsUybodkItiUKb/w1bsv8K8BA3jnHaf0yM1J5Bco2/ZnUaF0HJXLWVHMSBT0MQ1V3VrYUlDVA8AyoD7QGxjpHDYSuNJ53hv4SL1mAlVEpG6Qww5pa9IzuGH4bMrExfDp7WdQt3JZt0MyEazoYPXTTz9NSkoKycnJeDweYmOE+lXKWsKIYK4OhItIAnAaMAuorapbnZe24e2+Am9C2VjkbZucfUd+Vn8RmSMic9LT0wMWc6jZuPsg1w+fhaoy6vYzaFS9nNshmQh35A2BNngdXVwbCBeRCsAU4FlV/UJE9qpqlSKv71HVqiLyDfC8qk5z9k8CHlbVY450R8tA+NZ9h7jm3ZnsPZjD6P5n0LZeZbdDMsaEsZCdcisipYDPgVGq+oWze3tht5Pz5w5n/2agYZG3N3D2RbXt+7O4dthM9mTm8NFtXS1hGGOCIuhJw5kN9T6wTFVfKfLSBKCf87wfML7I/pvE6wxgX5FurKi040AW1743k/QD2Xx4axc6Nqxy4jcZY4wfuDF76mzgRmCRiPzu7HsUeB5IEZHbgPVAsvPad3in26bhnXJ7S3DDDS07M7K5/r1ZbN2bxchbu9CpcVW3QzLGRJGgJw1nbOJYk7W7H+V4Be4OaFBhYndmDjcMn8XGPQf54OYutjiNMSborIxImNh70Jsw1u7M5P1+iZzZrLrbIRljopAljTCw92AON74/m7QdGQy7qTNnN6/hdkjGmCjlxpiGOQk7M7K5Yfgs1uzM5N0bO3F+y5puh2SMiWKWNELYtn1ZXDd8Jlv2HmJEv0TOaWEtDGOMuyxphKjCO713Z+bw0a1dbdDbGBMSLGmEoDXpGVw/fBaZ2Xl8cntXuw/DGBMyLGmEmBXbDhyuJTWm/5m0qVfJ7ZCMMeYwSxohZMHGvdz8wWxKx8Uw6vYzaF6rotshGWPMn9iU2xDx05JtXDNsBuXLxJFy55mWMIwxIclaGiFg5G/rePLrJXSoX5nh/RJtASVjTMiypOGiggLlue+WMXzaWi5sU5vX+nakXGn7kRhjQpd1TwXRkCFD8Hg8AGTl5nP3p/MYPm0t7cvsYugNnSxhGGNCniWNIEpMTCQ5OZnxP07muvdm8sPibWTP/JQHLmhIbMyxajgaY0zosF9tgygpKYn/DRvNvRM2UqpiNQ79/BajX3z48LKZxhgT6qylESSqyvvT1vLCnBwqVazAppH/pv+lXSxhGGPCiiWNACg6dgGw71AuV7/6I898s5R21WPY/vEDPHxHX955550/HWeMMaHOkkYAFI5deDweFm7aS48hE5m7LZtzK+xg2v9uJOWTD3n66adJSUk5fJwxxoQDSxoBkJSUxJixY7nuyWFc+eZUtqen8/iZ5WlfOp2UlJTDXVJJSUmkpKSQmprqcsTGGOMbGwgPgOXb9vP2injKnHkDGWmzuaV1DLf36Qf0+MuxSUlJNq5hjAkb1tIopiPHLQC+nziZ3k+O5LLXp7Fiyx6yfx3Ona2VEUPftC4oY0xEsKRRTEXHLVSVl8ZM5M7xm1iQVYNz6sWw/YO7+fTZf/LMMzZ2YYyJHJY0iqlwPOKaO//FOY+O4s3fc2hcpwafDziLtgcXkvLxBzZ2YYyJOKKqbsfgd507d9Y5c+b45bOGDBlCYmLin8YdJk+ezFczlrGvbiJTVqZTkH2QLuXSGfP0XcTFWh42xoQnEZmrqp2Pd4x9w51A0W6o3PwCnhs1kX6fLmPCgQTmr0snZ87n9K2wjCnDBjP11yluh2uMMQEVNklDRHqKyAoRSRORQf7+/KMNbHs8HmbNTuX5YaO5/rlP6PjYVwxblEODxk24pW1ptg67g1GP3ciQZ56wcQtjTFQIi6QhIrHAW8AlQBvgWhFp489zFG1RbN+fxTOfTOTGtyfzSUZbnpmVTelOfdi1fjnd4lYw/bFLKLt1PimjR9m4hTEmqoTFmIaInAk8qaoXO9uPAKjq/452fHHHNMZ9N4kHvlpFTLWGAFQuDT3aNaBG3g5eGdifAbfeyDvvvPOnG/SMMSZSRNKYRn1gY5HtTc4+v7ry4iQaVa/AHs8HXB63iN+fupTLa+7mlfuuI+XjEVb6wxgT9cIlaZyQiPQXkTkiMic9Pb1YnzH11ymsGP4v7ru4DWPffZlffvmF1NRUK/1hjDGOcCkjshloWGS7gbPvMFUdBgwDb/fUyZ7A4/GQnJx8OEEkJSX9absoK/1hjIlW4dLSSAVaiEgTESkN9AUm+PUE1qIwxpgTCouBcAARuRT4PyAWGKGqzx7rWH/e3GeMMdHCl4HwcOmeQlW/A75zOw5jjIlm4dI9ZYwxJgRY0jDGGOMzSxrGGGN8ZknDGGOMz8Jm9tTJEJF0YP0JDqsB7AxCOKEqmq8/mq8dovv67dqPr7Gq1jzeARGZNHwhInNONLUskkXz9UfztUN0X79de8mv3bqnjDHG+MyShjHGGJ9Fc9IY5nYALovm64/ma4fovn679hKK2jENY4wxJy+aWxrGGGNOkiUNY4wxPovIpCEiPUVkhYikicigo7xeRkTGOq/PEpGEIq894uxfISIXBzNufyjutYtIdRHxiEiGiLwZ7Lj9pQTXf6GIzBWRRc6f3YIde0mV4Nq7iMjvzmOBiPQJduz+UJL/987rjZx//w8GK2Z/KcHPPkFEDhX5+Q894clUNaIeeEunrwaaAqWBBUCbI475BzDUed4XGOs8b+McXwZo4nxOrNvXFKRrLw+cA9wFvOn2tbhw/acB9Zzn7YDNbl9PEK+9HBDnPK8L7CjcDpdHSa6/yOvjgM+AB92+niD+7BOAxSdzvkhsaXQB0lR1jarmAGOA3kcc0xsY6TwfB3QXEXH2j1HVbFVdC6Q5nxcuin3tqpqpqtOArOCF63cluf75qrrF2b8EKCsiZYIStX+U5NoPqmqesz8eCMfZMSX5f4+IXAmsxfuzDzcluvaTFYlJoz6wscj2JmffUY9x/rPsA6r7+N5QVpJrjwT+uv6rgXmqmh2gOAOhRNcuIl1FZAmwCLirSBIJF8W+fhGpADwMPBWEOAOhpP/um4jIfBGZIiLnnuhkYbMIkzHBICJtgReAi9yOJZhUdRbQVkRaAyNF5HtVDedW58l4EnhVVTOK+ct3ONsKNFLVXSLSCfhKRNqq6v5jvSESWxqbgYZFths4+456jIjEAZWBXT6+N5SV5NojQYmuX0QaAF8CN6nq6oBH619++dmr6jIgA++4TjgpyfV3BYaIyDrgfuBREbkn0AH7UbGv3emK3wWgqnPxjo20PN7JIjFppAItRKSJiJTGO+gz4YhjJgD9nOd/Ayard1RoAtDXmWnQBGgBzA5S3P5QkmuPBMW+fhGpAnwLDFLV6UGL2H9Kcu1NnC8SRKQx0ApYF5yw/abY16+q56pqgqomAP8HPKeq4TSDsCQ/+5oiEgsgIk3xfuetOe7Z3B75D9BsgkuBlXiz5mPOvqeBXs7zeLyzJNLwJoWmRd77mPO+FcAlbl9LkK99HbAb72+amzhiBkY4PIp7/cB/gEzg9yKPWm5fT5Cu/Ua8A8C/A/OAK92+lmBe/xGf8SRhNnuqhD/7q4/42V9xonNZGRFjjDE+i8TuKWOMMQFiScMYY4zPLGkYY4zxmSUNY4wxPrOkYYwxxmeWNIw5ChGpIiL/KLJdT0TGBehcV4rIE8V4X2kR+bXwHgtjgsGm3BpzFE7p6G9UNeB3RovIb3jn0+8sxnsH4y1WN8r/kRnzV9bSMObongeaOWsMvOisO7AYQERuFpGvRGSiiKwTkXtE5AGn6NtMEanmHNdMRH4Q7/ocU0Wk1ZEnEZGWQHZhwhCRD0VkqIjMEZGVInK5s7+tiMx24lkoIi2cj/gKuD4YfyHGgBUsNOZYBgHtVLUjHG55FNUO7xoc8Xjvsn1YVU8TkVeBm/CWoxiGt2LsKhHpCrwNHLm409l478QtKgFvuetmgEdEmuNd5+Q1VR3llIqIdY5dDCSW7FKN8Z0lDWOKx6OqB4ADIrIP+NrZvwjo4JTbPgv4rEjl1KOtz1EXSD9iX4qqFgCrRGQN3lpQM4DHnKKKX6jqKgBVzReRHBGp6MRjTEBZ95QxxVN0rY2CItsFeH8ZiwH2qmrHIo/WR/mcQ3hbK0UdOdCoqvop0Ms5/jv583K0ZQjvxbNMGLGkYczRHQAqFvfN6l2PYK2I/B1AvE49yqHLgOZH7Pu7iMSISDO8S3iucCqQrlHV14HxQAfnc6sDO1U1t7ixGnMyLGkYcxTqXWNguogsFpEXi/kx1wO3icgCvJVEj1yCE+BX4LQjlt7cgLcS6fd4x0SygGRgsYj8jnc85SPn2CS8Jd2NCQqbcmuMy0TkNeBrVf1ZRD7EO9XXp3tCROQLvGuArAxkjMYUspaGMe57Dih3sm9yZlF9ZQnDBJO1NIwxxvjMWhrGGGN8ZknDGGOMzyxpGGOM8ZklDWOMMT6zpGGMMcZn/w8H60WEjCvpCAAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "# parse output file\n", - "output_file = 'otf_run.out'\n", - "otf_trajectory = otf_parser.OtfAnalysis(output_file)\n", - "\n", - "# plot temperature vs. simulation time\n", - "times = otf_trajectory.times\n", - "temps = otf_trajectory.thermostat['temperature']\n", - "dft_times = otf_trajectory.dft_times[1:] # exclude t = 0\n", - "\n", - "for n, dft_time in enumerate(dft_times):\n", - " otf_ind = times.index(dft_time)\n", - " plt.plot(dft_times[n], temps[otf_ind], 'kx')\n", - "\n", - "plt.plot(times, temps)\n", - "plt.xlabel('time (ps)')\n", - "plt.ylabel('temperature (K)')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "zutBC2A6f26C" - }, - "source": [ - "#### Comparing the learned model against ground truth." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 289 - }, - "id": "0lGByzonpH-3", - "outputId": "d09020af-779a-42b7-fa31-465d41672739" - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light", - "tags": [] - }, - "output_type": "display_data" - } - ], - "source": [ - "# create structures of 2 atoms, and plot the force on the second atom\n", - "cell = np.eye(3) * 1000\n", - "seps = np.arange(2, 5, 0.01)\n", - "specs = [[47, 47], [47, 53], [53, 53]]\n", - "store_frcs = np.zeros((3, len(seps)))\n", - "gp_frcs = np.zeros((3, len(seps)))\n", - "gp_stds = np.zeros((3, len(seps)))\n", - "for m, sep in enumerate(seps):\n", - " pos = np.array([[0, 0, 0], [sep, 0, 0]])\n", - " for n, spec_curr in enumerate(specs):\n", - " struc_curr = struc.Structure(cell, spec_curr, pos)\n", - " env_curr = env.AtomicEnvironment(struc_curr, 1, np.array([cutoff]))\n", - " frcs = \\\n", - " lj_module.run_dft_par(structure=struc_curr, dft_kwargs=lj_params)\n", - " store_frcs[n, m] = frcs[1, 0]\n", - "\n", - " # predict the x component of the force\n", - " pred, var = gp_model.predict(env_curr, 1)\n", - "\n", - " gp_frcs[n, m] = pred\n", - " gp_stds[n, m] = np.sqrt(var)\n", - "\n", - "# plot GP predictions vs ground truth\n", - "cols = ['b', 'r', 'g']\n", - "for n in range(3):\n", - " plt.plot(seps, store_frcs[n], color=cols[n], linestyle='-')\n", - " plt.plot(seps, gp_frcs[n], color=cols[n], linestyle='--')\n", - " plt.fill_between(seps, gp_frcs[n] + 3 * gp_stds[n],\n", - " gp_frcs[n] - 3 * gp_stds[n],\n", - " color=cols[n], alpha=0.4)\n", - "\n", - "plt.xlabel('separation ($\\AA$)')\n", - "plt.ylabel('force (eV/$\\AA$)')\n", - "plt.xlim(2, 3.4)\n", - "plt.ylim(-10, 15)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "yrjAH2-gHE80" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "FdXmBYZZHH4g" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "9LkZRNSQJ1-t" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Q8vPC6k2J6KS" - }, - "source": [ - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "MmU8G_FIJ9Ue" - }, - "source": [ - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "4HG-rQSfJ-wP" - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [ - "GeS6kyP8jBLX" - ], - "name": "FLARE_Tutorial.ipynb", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/docs/source/tutorials/colabs.rst b/docs/source/tutorials/colabs.rst new file mode 100644 index 000000000..218e3b6da --- /dev/null +++ b/docs/source/tutorials/colabs.rst @@ -0,0 +1,26 @@ +FLARE: Active Learning Bayesian Force Fields +============================================ + +We have a few Google Colab tutorials that you can check out and play with. + +`FLARE (ACE descriptors + sparse GP `_. +The tutorial shows how to run flare with ACE and SGP on energy and force data, demoing "offline" training on the MD17 dataset and "online" on-the-fly training of a simple aluminum force field. All the trainings use yaml files for configuration. + +`FLARE (ACE descriptors + sparse GP) with LAMMPS `_. +The tutorial shows how to compile LAMMPS with FLARE pair style and uncertainty compute code, and use LAMMPS for Bayesian active learning and uncertainty-aware molecular dynamics. + +`FLARE (ACE descriptors + sparse GP) Python API `_. +The tutorial shows how to do the offline and online trainings with python scripts. +A video walkthrough of the tutorial, including detailed discussion of expected outputs, is available `here `_. + +`FLARE (2+3-body + GP) `_. +The tutorial shows how to use flare 2+3 body descriptors and squared exponential kernel to train a Gaussian Process force field on-the-fly. + +`Compute thermal conductivity from FLARE and Boltzmann transport equations `_. +The tutorial shows how to use FLARE (LAMMPS) potential to compute lattice thermal conductivity from Boltzmann transport equation method, +with `Phono3py `_ for force constants calculations and `Phoebe `_ for thermal conductivities. + +`Using your own customized descriptors with FLARE `_. +The tutorial shows how to attach your own descriptors with FLARE sparse GP model and do training and testing. + +All the tutorials take a few minutes to run on a normal desktop computer or laptop (excluding installation time). diff --git a/docs/source/tutorials/gpfa.rst b/docs/source/tutorials/gpfa.rst index 83d8299c3..7984300cc 100644 --- a/docs/source/tutorials/gpfa.rst +++ b/docs/source/tutorials/gpfa.rst @@ -1,5 +1,11 @@ -Training a Gaussian Process from an AIMD Run -============================================ +Training a 2+3-body Gaussian Process from an AIMD Run +===================================================== + +This is a tutorial for using the GPFA (Gaussian process for AIMD) module with +2+3-body based GP model. A similar functionality is also supported in the offline +training with yaml configuration, which is shown in +`this colab tutorial `_. + Steven Torrisi (torrisi@g.harvard.edu), December 2019 In this tutorial, we'll demonstrate how a previously existing Ab-Initio diff --git a/docs/source/tutorials/prepare_data.rst b/docs/source/tutorials/prepare_data.rst deleted file mode 100644 index 6dc76b923..000000000 --- a/docs/source/tutorials/prepare_data.rst +++ /dev/null @@ -1,105 +0,0 @@ -Prepare your data -================= - -If you have collected data for training, including atomic positions, chemical -species, cell etc., you need to convert it into a list of ``Structure`` objects. -Below we provide a few examples. - - -VASP data ---------- - -If you have AIMD data from VASP, you can follow -`the step 2 of this instruction `_ -to generate ``Structure`` with the ``vasprun.xml`` file. - - -Data from Quantum Espresso, LAMMPS, etc. ----------------------------------------- - -If you have collected data from any -`calculator that ASE supports `_, -or have dumped data file of `format that ASE supports `_, -you can convert your data into ASE ``Atoms``, then from ``Atoms`` to -``Structure`` via ``Structure.from_ase_atoms``. - -For example, if you have collected data from QE, and obtained the QE output file ``.pwo``, -you can parse it with ASE, and convert ASE ``Atoms`` into ``Structure``. - - -.. code-block:: python - - from ase.io import read - from flare.struc import Structure - - frames = read('data.pwo', index=':', format='espresso-out') # read the whole traj - trajectory = [] - for atoms in frames: - trajectory.append(Structure.from_ase_atoms(atoms)) - - -If the data is from the LAMMPS dump file, use - -.. code-block:: python - - # if it's text file - frames = read('data.dump', index=':', format='lammps-dump-text') - - # if it's binary file - frames = read('data.dump', index=':', format='lammps-dump-binary') - - -Then the ``trajectory`` can be used to -`train GP from AIMD data `_. - - -Try building GP from data -------------------------- - -To have a more complete and better monitored training process, please use our -`GPFA module `_. - -Here we are not going to use this module, but only provide a simple example on -how the GP is constructed from the data. - -.. code-block:: python - - from flare.gp import GaussianProcess - from flare.utils.parameter_helper import ParameterHelper - - # set up hyperparameters, cutoffs - kernels = ['twobody', 'threebody'] - parameters = {'cutoff_twobody': 4.0, 'cutoff_threebody': 3.0} - pm = ParameterHelper(kernels=kernels, - random=True, - parameters=parameters) - hm = pm.as_dict() - hyps = hm['hyps'] - cutoffs = hm['cutoffs'] - hl = hm['hyp_labels'] - - kernel_type = 'mc' # multi-component. use 'sc' for single component system - - # build up GP model - gp_model = \ - GaussianProcess(kernels=kernels, - component=kernel_type, - hyps=hyps, - hyp_labels=hl, - cutoffs=cutoffs, - hyps_mask=hm, - parallel=False, - n_cpus=1) - - # feed training data into GP - # use the "trajectory" as from above, a list of Structure objects - for train_struc in trajectory: - gp_model.update_db(train_struc, forces) - gp_model.check_L_alpha() # build kernel matrix from training data - - # make a prediction with gp, test on a training data - test_env = gp_model.training_data[0] - gp_pred = gp_model.predict(test_env, 1) # obtain the x-component - # (force_x, var_x) - # x: 1, y: 2, z: 3 - print(gp_pred) diff --git a/docs/source/tutorials/tutorials.rst b/docs/source/tutorials/tutorials.rst index dedfa912b..384d5c96f 100644 --- a/docs/source/tutorials/tutorials.rst +++ b/docs/source/tutorials/tutorials.rst @@ -4,8 +4,6 @@ Tutorials .. toctree:: :maxdepth: 3 - aps_tutorial - prepare_data + colabs gpfa - ase after_training diff --git a/flare/_version.py b/flare/_version.py index 67bc602ab..4ded0a39f 100644 --- a/flare/_version.py +++ b/flare/_version.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.3.3" \ No newline at end of file diff --git a/flare/atoms.py b/flare/atoms.py index 16c246521..600a3257f 100644 --- a/flare/atoms.py +++ b/flare/atoms.py @@ -105,6 +105,8 @@ def stress(self): def stress(self, stress_array): if (stress_array is None) or (len(stress_array) == 6): self.label_setter("stress", stress_array) + elif len(stress_array) == 0: + self.label_setter("stress", None) else: raise ValueError("stress_array should be None or array of length 6") diff --git a/flare/bffs/sgp/sparse_gp.py b/flare/bffs/sgp/sparse_gp.py index 37f9a4fad..eb3259c25 100644 --- a/flare/bffs/sgp/sparse_gp.py +++ b/flare/bffs/sgp/sparse_gp.py @@ -4,11 +4,12 @@ from scipy.optimize import minimize from typing import List import warnings +from ase import Atoms from flare.atoms import FLARE_Atoms from flare.utils import NumpyEncoder try: - from ._C_flare import SparseGP, Structure, NormalizedDotProduct, B2 + from ._C_flare import SparseGP, Structure, NormalizedDotProduct, B2, DotProduct except Exception as e: warnings.warn(f"Cannot import _C_flare: {e.__class__.__name__}: {e}") @@ -50,6 +51,7 @@ def __init__( self.opt_method = opt_method self.bounds = bounds self.atom_indices = [] + self.rel_efs_noise = [] # Make placeholder hyperparameter labels. self.hyp_labels = [] @@ -59,7 +61,7 @@ def __init__( # prepare a new sGP for variance mapping self.sgp_var = None if isinstance( - kernels[0], NormalizedDotProduct + kernels[0], (NormalizedDotProduct, DotProduct) ): # TODO: adapt this to multiple kernels if kernels[0].power == 1: self.sgp_var_flag = "self" @@ -161,6 +163,8 @@ def as_dict(self): for kern in self.sparse_gp.kernels: if isinstance(kern, NormalizedDotProduct): kernel_list.append(("NormalizedDotProduct", kern.sigma, kern.power)) + elif isinstance(kern, DotProduct): + kernel_list.append(("DotProduct", kern.sigma, kern.power)) else: raise NotImplementedError out_dict["kernels"] = kernel_list @@ -170,6 +174,8 @@ def as_dict(self): for kern in self.sgp_var_kernels: if isinstance(kern, NormalizedDotProduct): kernel_list.append(("NormalizedDotProduct", kern.sigma, kern.power)) + elif isinstance(kern, DotProduct): + kernel_list.append(("DotProduct", kern.sigma, kern.power)) else: raise NotImplementedError out_dict["sgp_var_kernels"] = kernel_list @@ -191,6 +197,7 @@ def as_dict(self): ) train_struc.forces = struc_cpp.forces.reshape((struc_cpp.noa, 3)) train_struc.stress = struc_cpp.stresses + train_struc.info["rel_efs_noise"] = np.array(self.rel_efs_noise[s]) # Add back the single atom energies to dump the original energy single_atom_sum = 0 @@ -214,10 +221,13 @@ def from_dict(in_dict): kernel_list = in_dict["kernels"] assert len(kernel_list) == 1 kernel_hyps = kernel_list[0] - assert kernel_hyps[0] == "NormalizedDotProduct" + assert kernel_hyps[0] in ["NormalizedDotProduct", "DotProduct"] sigma = float(kernel_hyps[1]) power = int(kernel_hyps[2]) - kernel = NormalizedDotProduct(sigma, power) + if kernel_hyps[0] == "NormalizedDotProduct": + kernel = NormalizedDotProduct(sigma, power) + elif kernel_hyps[0] == "DotProduct": + kernel = DotProduct(sigma, power) kernels = [kernel] # Recover descriptor from checkpoint. @@ -275,6 +285,9 @@ def from_dict(in_dict): else: energy = None + rel_efs_noise = train_struc.info.get("rel_efs_noise", [1, 1, 1]) + rel_e_noise, rel_f_noise, rel_s_noise = rel_efs_noise + gp.update_db( train_struc, train_struc.forces, @@ -285,6 +298,9 @@ def from_dict(in_dict): sgp=None, update_qr=False, atom_indices=atom_indices, + rel_e_noise=rel_e_noise, + rel_f_noise=rel_f_noise, + rel_s_noise=rel_s_noise, ) gp.sparse_gp.update_matrices_QR() @@ -307,10 +323,13 @@ def update_db( sgp=None, # for creating sgp_var update_qr=True, atom_indices=[-1], + rel_e_noise: float = 1, + rel_f_noise: float = 1, + rel_s_noise: float = 1, ): # Convert coded species to 0, 1, 2, etc. - if isinstance(structure, FLARE_Atoms): + if isinstance(structure, (Atoms, FLARE_Atoms)): coded_species = [] for spec in structure.numbers: coded_species.append(self.species_map[spec]) @@ -351,7 +370,10 @@ def update_db( sgp = self.sparse_gp self.atom_indices.append(atom_indices) - sgp.add_training_structure(structure_descriptor, atom_indices) + sgp.add_training_structure( + structure_descriptor, atom_indices, rel_e_noise, rel_f_noise, rel_s_noise + ) + self.rel_efs_noise.append([rel_e_noise, rel_f_noise, rel_s_noise]) if mode == "all": if not custom_range: sgp.add_all_environments(structure_descriptor) @@ -406,10 +428,10 @@ def write_varmap_coefficients(self, filename, contributor, kernel_idx): assert (len(old_kernels) == 1) and ( kernel_idx == 0 ), "Not support multiple kernels" - assert isinstance(old_kernels[0], NormalizedDotProduct) + assert isinstance(old_kernels[0], (NormalizedDotProduct, DotProduct)) power = 1 - new_kernels = [NormalizedDotProduct(old_kernels[0].sigma, power)] + new_kernels = [old_kernels[0].__class__(old_kernels[0].sigma, power)] # Build a power=1 SGP from scratch if self.sgp_var is None: @@ -450,6 +472,10 @@ def write_varmap_coefficients(self, filename, contributor, kernel_idx): mode="specific", sgp=self.sgp_var, update_qr=False, + atom_indices=self.atom_indices[s + n_sgp_var], + rel_e_noise=self.rel_efs_noise[s + n_sgp_var][0], + rel_f_noise=self.rel_efs_noise[s + n_sgp_var][1], + rel_s_noise=self.rel_efs_noise[s + n_sgp_var][2], ) self.sgp_var.update_matrices_QR() @@ -479,12 +505,12 @@ def duplicate(self, new_hyps=None, new_kernels=None, new_powers=None): assert len(hyps) == len(self.sparse_gp.kernels) + 3 kernels = [] for k, kern in enumerate(self.sparse_gp.kernels): - assert isinstance(kern, NormalizedDotProduct) + assert isinstance(kern, (NormalizedDotProduct, DotProduct)) if new_powers is not None: power = new_powers[k] else: power = kern.power - kernels.append(NormalizedDotProduct(hyps[k], power)) + kernels.append(kern.__class__(hyps[k], power)) else: kernels = new_kernels @@ -517,6 +543,10 @@ def duplicate(self, new_hyps=None, new_kernels=None, new_powers=None): mode="specific", sgp=new_gp, update_qr=False, + atom_indices=self.atom_indices[s], + rel_e_noise=self.rel_efs_noise[s][0], + rel_f_noise=self.rel_efs_noise[s][1], + rel_s_noise=self.rel_efs_noise[s][2], ) new_gp.update_matrices_QR() @@ -607,7 +637,7 @@ def optimize_hyperparameters( initial_guess = sparse_gp.hyperparameters precompute = True for kern in sparse_gp.kernels: - if not isinstance(kern, NormalizedDotProduct): + if not isinstance(kern, (NormalizedDotProduct, DotProduct)): precompute = False break if precompute: diff --git a/flare/io/output.py b/flare/io/output.py index 2c23a9019..db0382146 100644 --- a/flare/io/output.py +++ b/flare/io/output.py @@ -18,7 +18,7 @@ import flare from ase.data import chemical_symbols from ase.io import write - +from ase.calculators.calculator import PropertyNotImplementedError # Unit conversions. eva_to_gpa = 160.21766208 @@ -277,14 +277,21 @@ def write_md_config( string += "\n" + # Check if we need to report the stress and pressure tensors + try: + if type(structure.stress) == np.ndarray: + stress_exist = True + except PropertyNotImplementedError: + stress_exist = False + # Report cell if stress attribute is present. - if structure.stress is not None: + if stress_exist and structure.stress is not None: string += "Periodic cell (A): \n" string += str(np.array(structure.cell)) + "\n\n" # Report stress tensor. pressure = None - if structure.stress is not None: + if stress_exist and structure.stress is not None: stress_tensor = structure.stress * eva_to_gpa # Convert to GPa s8 = " " * 8 string += "Stress tensor (GPa):\n" @@ -308,7 +315,7 @@ def write_md_config( pressure = (stress_tensor[0] + stress_tensor[1] + stress_tensor[2]) / 3 # Report stress tensor uncertainties. - if structure.stress_stds is not None: + if stress_exist and structure.stress_stds is not None: stress_stds = structure.stress_stds * eva_to_gpa # Convert to GPa string += "Stress tensor uncertainties (GPa):\n" for p in range(6): diff --git a/flare/learners/otf.py b/flare/learners/otf.py index 22afacc2c..89083d435 100644 --- a/flare/learners/otf.py +++ b/flare/learners/otf.py @@ -29,6 +29,7 @@ from flare.md.fake import FakeMD from ase import units from ase.io import read, write +from ase.calculators.calculator import PropertyNotImplementedError from flare.io.output import Output, compute_mae from flare.learners.utils import is_std_in_bound, get_env_indices @@ -371,7 +372,11 @@ def run(self): self.run_dft() dft_forces = deepcopy(self.atoms.forces) - dft_stress = deepcopy(self.atoms.stress) + # some ase calculators don't have the stress property implemented + try: + dft_stress = deepcopy(self.atoms.stress) + except PropertyNotImplementedError: + dft_stress = None dft_energy = self.atoms.potential_energy # run MD step & record the state @@ -462,7 +467,13 @@ def initialize_train(self): # call dft and update positions self.run_dft() dft_frcs = deepcopy(self.atoms.forces) - dft_stress = deepcopy(self.atoms.stress) + + # some ase calculators don't have the stress property implemented + try: + dft_stress = deepcopy(self.atoms.stress) + except PropertyNotImplementedError: + dft_stress = None + dft_energy = self.atoms.potential_energy self.update_temperature() diff --git a/flare/md/fake.py b/flare/md/fake.py index 7782734b8..bd3d46a2f 100644 --- a/flare/md/fake.py +++ b/flare/md/fake.py @@ -40,6 +40,11 @@ def __init__( all_data = [] for fn in filenames: trj = read(fn, format=format, index=":", **io_kwargs) + + # preprocessing: remove "step" keyword from trj + for frame in trj: + frame.info.pop("step", None) + self.stat_num.append(len(trj)) all_data += trj write("All_Data.xyz", all_data) diff --git a/flare/scripts/otf_train.py b/flare/scripts/otf_train.py index 899eee185..b2b998239 100644 --- a/flare/scripts/otf_train.py +++ b/flare/scripts/otf_train.py @@ -229,6 +229,10 @@ def get_sgp_calc(flare_config): cutoff = flare_config["cutoff"] descriptors = [] for d in flare_config["descriptors"]: + if "cutoff_matrix" in d: # multiple cutoffs + assert np.allclose(np.array(d["cutoff_matrix"]).shape, (n_species, n_species)),\ + "cutoff_matrix needs to be of shape (n_species, n_species)" + if d["name"] == "B2": radial_hyps = [0.0, cutoff] cutoff_hyps = [] @@ -285,6 +289,8 @@ def get_sgp_calc(flare_config): sae_dct ), "'single_atom_energies' should be the same length as 'species'" single_atom_energies = {i: sae_dct[i] for i in range(n_species)} + else: + single_atom_energies = {i: 0 for i in range(n_species)} sgp = SGP_Wrapper( kernels=kernels, diff --git a/lammps_plugins/compute_flare_std_atom.cpp b/lammps_plugins/compute_flare_std_atom.cpp index 7ad52b201..1b809d6c2 100644 --- a/lammps_plugins/compute_flare_std_atom.cpp +++ b/lammps_plugins/compute_flare_std_atom.cpp @@ -62,10 +62,10 @@ ComputeFlareStdAtom::~ComputeFlareStdAtom() { memory->destroy(beta); -// if (allocated) { -// memory->destroy(setflag); -// memory->destroy(cutsq); -// } + if (allocated) { + memory->destroy(setflag); + memory->destroy(cutsq); + } memory->destroy(stds); memory->destroy(desc_derv); @@ -182,22 +182,28 @@ void ComputeFlareStdAtom::compute_peratom() { if (use_map) { int power = 2; compute_energy_and_u(B2_vals, B2_norm_squared, single_bond_vals, power, - n_species, n_max, l_max, beta_matrices[itype - 1], u, &variance); + n_species, n_max, l_max, beta_matrices[itype - 1], u, &variance, normalized); variance /= sig2; } else { + Eigen::VectorXd kernel_vec = Eigen::VectorXd::Zero(n_clusters); + double K_self; double B2_norm = pow(B2_norm_squared, 0.5); Eigen::VectorXd normed_B2 = B2_vals / B2_norm; - Eigen::VectorXd kernel_vec = Eigen::VectorXd::Zero(n_clusters); int cum_types = 0; for (int s = 0; s < n_types; s++) { if (type[i] - 1 == s) { - kernel_vec.segment(cum_types, n_clusters_by_type[s]) = (normed_sparse_descriptors[s] * normed_B2).array().pow(power); + if (normalized) { + kernel_vec.segment(cum_types, n_clusters_by_type[s]) = (normed_sparse_descriptors[s] * normed_B2).array().pow(power); + K_self = 1.0; + } else { + // the normed_sparse_descriptors is non-normalized in this case + kernel_vec.segment(cum_types, n_clusters_by_type[s]) = (normed_sparse_descriptors[s] * B2_vals).array().pow(power); + K_self = pow(B2_norm_squared, power); + } } cum_types += n_clusters_by_type[s]; } Eigen::VectorXd L_inv_kv = L_inv_blocks[0] * kernel_vec; - - double K_self = 1.0; double Q_self = sig2 * L_inv_kv.transpose() * L_inv_kv; variance = K_self - Q_self; @@ -266,16 +272,13 @@ double ComputeFlareStdAtom::memory_usage() void ComputeFlareStdAtom::allocate() { allocated = 1; -// int n = atom->ntypes; -// -// memory->create(setflag, n + 1, n + 1, "compute:setflag"); -// -// // Set the diagonal of setflag to 1 (otherwise pair.cpp will throw an error) -// for (int i = 1; i <= n; i++) -// setflag[i][i] = 1; -// -// // Create cutsq array (used in pair.cpp) -// memory->create(cutsq, n + 1, n + 1, "compute:cutsq"); + int n = atom->ntypes; + + memory->create(setflag, n + 1, n + 1, "compute:setflag"); + + // Set the diagonal of setflag to 1 (otherwise pair.cpp will throw an error) + for (int i = 1; i <= n; i++) + setflag[i][i] = 1; } /* ---------------------------------------------------------------------- @@ -326,6 +329,10 @@ void ComputeFlareStdAtom::parse_cutoff_matrix(int n_species, FILE *fptr){ grab(fptr, n_cutoffs, cutoffs); MPI_Bcast(cutoffs, n_cutoffs, MPI_DOUBLE, 0, world); + // Create cutsq array (used in pair.cpp) + memory->create(cutsq, n_species + 1, n_species + 1, "compute:cutsq"); + memset(&cutsq[0][0], 0, (n_species + 1) * (n_species + 1) * sizeof(double)); + // Fill in the cutoff matrix. cutoff = -1; cutoff_matrix = Eigen::MatrixXd::Zero(n_species, n_species); @@ -334,6 +341,7 @@ void ComputeFlareStdAtom::parse_cutoff_matrix(int n_species, FILE *fptr){ for (int j = 0; j < n_species; j++){ double cutoff_val = cutoffs[cutoff_count]; cutoff_matrix(i, j) = cutoff_val; + cutsq[i + 1][j + 1] = cutoff_val * cutoff_val; if (cutoff_val > cutoff) cutoff = cutoff_val; cutoff_count ++; } @@ -343,8 +351,8 @@ void ComputeFlareStdAtom::parse_cutoff_matrix(int n_species, FILE *fptr){ void ComputeFlareStdAtom::read_file(char *filename) { int me = comm->me; - char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE]; - int radial_string_length, cutoff_string_length; + char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE], kernel_string[MAXLINE]; + int radial_string_length, cutoff_string_length, kernel_string_length; FILE *fptr; // Check that the potential file can be opened. @@ -375,6 +383,10 @@ void ComputeFlareStdAtom::read_file(char *filename) { hyperparameters(2) = fn; hyperparameters(3) = sn; + fgets(line, MAXLINE, fptr); + sscanf(line, "%s", kernel_string); // kernel name + kernel_string_length = strlen(kernel_string); + fgets(line, MAXLINE, fptr); sscanf(line, "%s", radial_string); // Radial basis set radial_string_length = strlen(radial_string); @@ -393,8 +405,10 @@ void ComputeFlareStdAtom::read_file(char *filename) { MPI_Bcast(&cutoff, 1, MPI_DOUBLE, 0, world); MPI_Bcast(&radial_string_length, 1, MPI_INT, 0, world); MPI_Bcast(&cutoff_string_length, 1, MPI_INT, 0, world); + MPI_Bcast(&kernel_string_length, 1, MPI_INT, 0, world); MPI_Bcast(radial_string, radial_string_length + 1, MPI_CHAR, 0, world); MPI_Bcast(cutoff_string, cutoff_string_length + 1, MPI_CHAR, 0, world); + MPI_Bcast(kernel_string, kernel_string_length + 1, MPI_CHAR, 0, world); // Parse the cutoffs and fill in the cutoff matrix parse_cutoff_matrix(n_species, fptr); @@ -420,6 +434,13 @@ void ComputeFlareStdAtom::read_file(char *filename) { else if (!strcmp(cutoff_string, "cosine")) cutoff_function = cos_cutoff; + // Set the kernel + if (!strcmp(kernel_string, "NormalizedDotProduct")) { + normalized = true; + } else { + normalized = false; + } + // Parse the beta vectors. //memory->create(beta, beta_size * n_species * n_species, "compute:beta"); memory->create(beta, beta_size * n_species, "compute:beta"); @@ -454,8 +475,8 @@ void ComputeFlareStdAtom::read_file(char *filename) { void ComputeFlareStdAtom::read_L_inverse(char *filename) { int me = comm->me; - char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE]; - int radial_string_length, cutoff_string_length; + char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE], kernel_string[MAXLINE]; + int radial_string_length, cutoff_string_length, kernel_string_length; FILE *fptr; // Check that the potential file can be opened. @@ -473,7 +494,8 @@ void ComputeFlareStdAtom::read_L_inverse(char *filename) { fgets(line, MAXLINE, fptr); // skip the first line fgets(line, MAXLINE, fptr); // power - sscanf(line, "%i", &power); + sscanf(line, "%i %s", &power, kernel_string); + kernel_string_length = strlen(kernel_string); fgets(line, MAXLINE, fptr); // hyperparameters sscanf(line, "%i", &n_hyps); @@ -509,10 +531,12 @@ void ComputeFlareStdAtom::read_L_inverse(char *filename) { MPI_Bcast(&l_max, 1, MPI_INT, 0, world); MPI_Bcast(&n_kernels, 1, MPI_INT, 0, world); MPI_Bcast(&cutoff, 1, MPI_DOUBLE, 0, world); + MPI_Bcast(&kernel_string_length, 1, MPI_INT, 0, world); MPI_Bcast(&radial_string_length, 1, MPI_INT, 0, world); MPI_Bcast(&cutoff_string_length, 1, MPI_INT, 0, world); MPI_Bcast(radial_string, radial_string_length + 1, MPI_CHAR, 0, world); MPI_Bcast(cutoff_string, cutoff_string_length + 1, MPI_CHAR, 0, world); + MPI_Bcast(kernel_string, kernel_string_length + 1, MPI_CHAR, 0, world); // Parse the cutoffs and fill in the cutoff matrix parse_cutoff_matrix(n_species, fptr); @@ -545,6 +569,13 @@ void ComputeFlareStdAtom::read_L_inverse(char *filename) { else if (!strcmp(cutoff_string, "cosine")) cutoff_function = cos_cutoff; + // Set the kernel + if (!strcmp(kernel_string, "NormalizedDotProduct")) { + normalized = true; + } else { + normalized = false; + } + // Parse the beta vectors. memory->create(beta, Linv_size, "compute:L_inv"); if (me == 0) diff --git a/lammps_plugins/compute_flare_std_atom.h b/lammps_plugins/compute_flare_std_atom.h index 283c3f419..5c25920bd 100644 --- a/lammps_plugins/compute_flare_std_atom.h +++ b/lammps_plugins/compute_flare_std_atom.h @@ -56,7 +56,7 @@ class ComputeFlareStdAtom : public Compute { Eigen::VectorXd hyperparameters; std::vector L_inv_blocks, normed_sparse_descriptors; int n_hyps, n_clusters, n_kernels, n_types; - bool use_map = false; + bool use_map = false, normalized; int power = 2; int* n_clusters_by_type; diff --git a/lammps_plugins/kokkos/pair_flare_kokkos.cpp b/lammps_plugins/kokkos/pair_flare_kokkos.cpp index 995c141bc..76653df28 100644 --- a/lammps_plugins/kokkos/pair_flare_kokkos.cpp +++ b/lammps_plugins/kokkos/pair_flare_kokkos.cpp @@ -721,6 +721,12 @@ void PairFLAREKokkos::coeff(int narg, char **arg) { PairFLARE::coeff(narg,arg); + if(!normalized) + error->all(FLERR, "for now, pair flare/kk only supports the normalized kernel"); + if(power != 2) + error->all(FLERR, "for now, pair flare/kk only supports the power-2 kernel"); + //TODO check chebyshev and quadratic + n_harmonics = (l_max+1)*(l_max+1); n_radial = n_species * n_max; n_bond = n_radial * n_harmonics; @@ -777,7 +783,7 @@ void PairFLAREKokkos::init_style() if (memstr != NULL) { maxmem = std::atof(memstr) * 1.0e9; } - printf("FLARE will use up to %.2f GB of device memory, controlled by MAXMEM environment variable\n", maxmem/1.0e9); + if(comm->me==0 || comm->me==comm->nprocs-1) printf("FLARE will use up to %.2f GB of device memory, controlled by MAXMEM environment variable\n", maxmem/1.0e9); } diff --git a/lammps_plugins/lammps_descriptor.cpp b/lammps_plugins/lammps_descriptor.cpp index b2e9ed57e..e2aa0312a 100644 --- a/lammps_plugins/lammps_descriptor.cpp +++ b/lammps_plugins/lammps_descriptor.cpp @@ -252,21 +252,32 @@ void compute_energy_and_u(Eigen::VectorXd &B2_vals, const Eigen::VectorXd &single_bond_vals, int power, int n_species, int N, int lmax, const Eigen::MatrixXd &beta_matrix, - Eigen::VectorXd &u, double *evdwl) { + Eigen::VectorXd &u, double *evdwl, bool normalized) { int n1_l, n2_l, counter, n1_count, n2_count; int n_radial = n_species * N; int n_harmonics = (lmax + 1) * (lmax + 1); Eigen::VectorXd w; - if (power == 1) { - double B2_norm = pow(norm_squared, 0.5); - *evdwl = B2_vals.dot(beta_matrix.col(0)) / B2_norm; - w = beta_matrix.col(0) / B2_norm - *evdwl * B2_vals / norm_squared; - } else if (power == 2) { - Eigen::VectorXd beta_p = beta_matrix * B2_vals; - *evdwl = B2_vals.dot(beta_p) / norm_squared; - w = 2 * (beta_p - *evdwl * B2_vals) / norm_squared; + if (normalized) { + if (power == 1) { + double B2_norm = pow(norm_squared, 0.5); + *evdwl = B2_vals.dot(beta_matrix.col(0)) / B2_norm; + w = beta_matrix.col(0) / B2_norm - *evdwl * B2_vals / norm_squared; + } else if (power == 2) { + Eigen::VectorXd beta_p = beta_matrix * B2_vals; + *evdwl = B2_vals.dot(beta_p) / norm_squared; + w = 2 * (beta_p - *evdwl * B2_vals) / norm_squared; + } + } else { + if (power == 1) { + w = beta_matrix.col(0); + *evdwl = B2_vals.dot(w); + } else if (power == 2) { + Eigen::VectorXd beta_p = beta_matrix * B2_vals; + *evdwl = B2_vals.dot(beta_p); + w = 2 * beta_p; + } } // Compute u(n1, l, m), where f_ik = u * dA/dr_ik diff --git a/lammps_plugins/lammps_descriptor.h b/lammps_plugins/lammps_descriptor.h index 5a3872a64..411a41f88 100644 --- a/lammps_plugins/lammps_descriptor.h +++ b/lammps_plugins/lammps_descriptor.h @@ -46,6 +46,6 @@ void compute_energy_and_u(Eigen::VectorXd &B2_vals, const Eigen::VectorXd &single_bond_vals, int power, int n_species, int N, int lmax, const Eigen::MatrixXd &beta_matrix, - Eigen::VectorXd &u, double *evdwl); + Eigen::VectorXd &u, double *evdwl, bool normalized); #endif diff --git a/lammps_plugins/pair_flare.cpp b/lammps_plugins/pair_flare.cpp index cec037122..a3949f45b 100644 --- a/lammps_plugins/pair_flare.cpp +++ b/lammps_plugins/pair_flare.cpp @@ -129,7 +129,7 @@ void PairFLARE::compute(int eflag, int vflag) { single_bond_vals, n_species, n_max, l_max); compute_energy_and_u(B2_vals, B2_norm_squared, single_bond_vals, power, - n_species, n_max, l_max, beta_matrices[itype - 1], u, &evdwl); + n_species, n_max, l_max, beta_matrices[itype - 1], u, &evdwl, normalized); // Continue if the environment is empty. if (B2_norm_squared < empty_thresh) @@ -262,8 +262,8 @@ double PairFLARE::init_one(int i, int j) { void PairFLARE::read_file(char *filename) { int me = comm->me; - char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE]; - int radial_string_length, cutoff_string_length; + char line[MAXLINE], radial_string[MAXLINE], cutoff_string[MAXLINE], kernel_string[MAXLINE]; + int radial_string_length, cutoff_string_length, kernel_string_length; FILE *fptr; // Check that the potential file can be opened. @@ -281,7 +281,8 @@ void PairFLARE::read_file(char *filename) { fgets(line, MAXLINE, fptr); // Date and contributor fgets(line, MAXLINE, fptr); // Power, use integer instead of double for simplicity - sscanf(line, "%i", &power); + sscanf(line, "%i %s", &power, &kernel_string); + kernel_string_length = strlen(kernel_string); fgets(line, MAXLINE, fptr); sscanf(line, "%s", radial_string); // Radial basis set @@ -303,8 +304,10 @@ void PairFLARE::read_file(char *filename) { MPI_Bcast(&cutoff, 1, MPI_DOUBLE, 0, world); MPI_Bcast(&radial_string_length, 1, MPI_INT, 0, world); MPI_Bcast(&cutoff_string_length, 1, MPI_INT, 0, world); + MPI_Bcast(&kernel_string_length, 1, MPI_INT, 0, world); MPI_Bcast(radial_string, radial_string_length + 1, MPI_CHAR, 0, world); MPI_Bcast(cutoff_string, cutoff_string_length + 1, MPI_CHAR, 0, world); + MPI_Bcast(kernel_string, kernel_string_length + 1, MPI_CHAR, 0, world); // Parse the cutoffs. int n_cutoffs = n_species * n_species; @@ -313,6 +316,10 @@ void PairFLARE::read_file(char *filename) { grab(fptr, n_cutoffs, cutoffs); MPI_Bcast(cutoffs, n_cutoffs, MPI_DOUBLE, 0, world); + // Create cutsq array (used in pair.cpp) + memory->create(cutsq, n_species + 1, n_species + 1, "pair:cutsq"); + memset(&cutsq[0][0], 0, (n_species + 1) * (n_species + 1) * sizeof(double)); + // Fill in the cutoff matrix. cutoff = -1; cutoff_matrix = Eigen::MatrixXd::Zero(n_species, n_species); @@ -321,6 +328,7 @@ void PairFLARE::read_file(char *filename) { for (int j = 0; j < n_species; j++){ double cutoff_val = cutoffs[cutoff_count]; cutoff_matrix(i, j) = cutoff_val; + cutsq[i + 1][j + 1] = cutoff_val * cutoff_val; if (cutoff_val > cutoff) cutoff = cutoff_val; cutoff_count ++; } @@ -354,6 +362,17 @@ void PairFLARE::read_file(char *filename) { else if (!strcmp(cutoff_string, "cosine")) cutoff_function = cos_cutoff; + // Set the kernel + if (strcmp(kernel_string, "NormalizedDotProduct") == 0) { + normalized = true; + } + else if (strcmp(kernel_string, "DotProduct") == 0){ + normalized = false; + } + else { + error->all(FLERR, "Kernel string not recognized, expected "); + } + // Parse the beta vectors. memory->create(beta, beta_size * n_species, "pair:beta"); if (me == 0) diff --git a/lammps_plugins/pair_flare.h b/lammps_plugins/pair_flare.h index 456523108..3107faedc 100644 --- a/lammps_plugins/pair_flare.h +++ b/lammps_plugins/pair_flare.h @@ -29,6 +29,7 @@ class PairFLARE : public Pair { protected: int power, n_species, n_max, l_max, n_descriptors, beta_size; + bool normalized; std::function &, std::vector &, double, int, std::vector)> diff --git a/src/ace_binding.cpp b/src/ace_binding.cpp index 9aa7b28ca..71c611148 100644 --- a/src/ace_binding.cpp +++ b/src/ace_binding.cpp @@ -12,6 +12,7 @@ #include "four_body.h" #include "squared_exponential.h" #include "normalized_dot_product.h" +#include "dot_product.h" #include "norm_dot_icm.h" #include @@ -148,6 +149,16 @@ PYBIND11_MODULE(_C_flare, m) { .def("envs_struc", &NormalizedDotProduct::envs_struc) .def("struc_struc", &NormalizedDotProduct::struc_struc); + py::class_(m, "DotProduct") + .def(py::init()) + .def_readonly("sigma", &DotProduct::sigma) + .def_readwrite("power", &DotProduct::power) + .def_readonly("kernel_hyperparameters", + &DotProduct::kernel_hyperparameters) + .def("envs_envs", &DotProduct::envs_envs) + .def("envs_struc", &DotProduct::envs_struc) + .def("struc_struc", &DotProduct::struc_struc); + py::class_(m, "NormalizedDotProduct_ICM") .def(py::init()); @@ -171,7 +182,10 @@ PYBIND11_MODULE(_C_flare, m) { &SparseGP::add_uncertain_environments) .def("add_training_structure", &SparseGP::add_training_structure, py::arg("structure"), - py::arg("atom_indices") = - Eigen::VectorXi::Ones(1)) + py::arg("atom_indices") = - Eigen::VectorXi::Ones(1), + py::arg("rel_e_noise") = 1.0, + py::arg("rel_f_noise") = 1.0, + py::arg("rel_s_noise") = 1.0) .def("update_matrices_QR", &SparseGP::update_matrices_QR) .def("compute_likelihood", &SparseGP::compute_likelihood) .def("compute_likelihood_stable", &SparseGP::compute_likelihood_stable) diff --git a/src/flare_pp/bffs/sparse_gp.cpp b/src/flare_pp/bffs/sparse_gp.cpp index 3ca5452ee..d04053ea0 100644 --- a/src/flare_pp/bffs/sparse_gp.cpp +++ b/src/flare_pp/bffs/sparse_gp.cpp @@ -462,7 +462,10 @@ void SparseGP ::update_Kuf( } void SparseGP ::add_training_structure(const Structure &structure, - const std::vector atom_indices) { + const std::vector atom_indices, + double rel_e_noise, + double rel_f_noise, + double rel_s_noise) { // Allow adding a subset of force labels initialize_sparse_descriptors(structure); @@ -496,11 +499,11 @@ void SparseGP ::add_training_structure(const Structure &structure, // Update noise. noise_vector.conservativeResize(n_labels + n_struc_labels); noise_vector.segment(n_labels, n_energy) = - Eigen::VectorXd::Constant(n_energy, 1 / (energy_noise * energy_noise)); + Eigen::VectorXd::Constant(n_energy, 1 / (energy_noise * energy_noise * rel_e_noise * rel_e_noise)); noise_vector.segment(n_labels + n_energy, n_force) = - Eigen::VectorXd::Constant(n_force, 1 / (force_noise * force_noise)); + Eigen::VectorXd::Constant(n_force, 1 / (force_noise * force_noise * rel_f_noise * rel_f_noise)); noise_vector.segment(n_labels + n_energy + n_force, n_stress) = - Eigen::VectorXd::Constant(n_stress, 1 / (stress_noise * stress_noise)); + Eigen::VectorXd::Constant(n_stress, 1 / (stress_noise * stress_noise * rel_s_noise * rel_s_noise)); // Save "1" vector for energy, force and stress noise, for likelihood gradient calculation e_noise_one.conservativeResize(n_labels + n_struc_labels); @@ -511,9 +514,27 @@ void SparseGP ::add_training_structure(const Structure &structure, f_noise_one.segment(n_labels, n_struc_labels) = Eigen::VectorXd::Zero(n_struc_labels); s_noise_one.segment(n_labels, n_struc_labels) = Eigen::VectorXd::Zero(n_struc_labels); - e_noise_one.segment(n_labels, n_energy) = Eigen::VectorXd::Ones(n_energy); - f_noise_one.segment(n_labels + n_energy, n_force) = Eigen::VectorXd::Ones(n_force); - s_noise_one.segment(n_labels + n_energy + n_force, n_stress) = Eigen::VectorXd::Ones(n_stress); + e_noise_one.segment(n_labels, n_energy) = + Eigen::VectorXd::Constant(n_energy, 1 / (rel_e_noise * rel_e_noise)); + f_noise_one.segment(n_labels + n_energy, n_force) = + Eigen::VectorXd::Constant(n_force, 1 / (rel_f_noise * rel_f_noise)); + s_noise_one.segment(n_labels + n_energy + n_force, n_stress) = + Eigen::VectorXd::Constant(n_stress, 1 / (rel_s_noise * rel_s_noise)); + + inv_e_noise_one.conservativeResize(n_labels + n_struc_labels); + inv_f_noise_one.conservativeResize(n_labels + n_struc_labels); + inv_s_noise_one.conservativeResize(n_labels + n_struc_labels); + + inv_e_noise_one.segment(n_labels, n_struc_labels) = Eigen::VectorXd::Zero(n_struc_labels); + inv_f_noise_one.segment(n_labels, n_struc_labels) = Eigen::VectorXd::Zero(n_struc_labels); + inv_s_noise_one.segment(n_labels, n_struc_labels) = Eigen::VectorXd::Zero(n_struc_labels); + + inv_e_noise_one.segment(n_labels, n_energy) = + Eigen::VectorXd::Constant(n_energy, rel_e_noise * rel_e_noise); + inv_f_noise_one.segment(n_labels + n_energy, n_force) = + Eigen::VectorXd::Constant(n_force, rel_f_noise * rel_f_noise); + inv_s_noise_one.segment(n_labels + n_energy + n_force, n_stress) = + Eigen::VectorXd::Constant(n_stress, rel_s_noise * rel_s_noise); // Update Kuf kernels. Eigen::MatrixXd envs_struc_kernels; @@ -741,9 +762,10 @@ double SparseGP ::compute_likelihood_gradient_stable(bool precomputed_KnK) { constant_term = -(1. / 2.) * n_labels * log(2 * M_PI); // Compute complexity penalty. - double noise_det = - 2 * (n_energy_labels * log(abs(energy_noise)) - + n_force_labels * log(abs(force_noise)) - + n_stress_labels * log(abs(stress_noise))); + double noise_det = 0; + for (int i = 0; i < noise_vector.size(); i++) { + noise_det += log(noise_vector(i)); + } assert(L_diag.size() == R_inv_diag.size()); double Kuu_inv_det = 0; @@ -838,11 +860,11 @@ double SparseGP ::compute_likelihood_gradient_stable(bool precomputed_KnK) { double sn3 = stress_noise * stress_noise * stress_noise; compute_KnK(precomputed_KnK); - complexity_grad(hyp_index + 0) = - e_noise_one.sum() / energy_noise + complexity_grad(hyp_index + 0) = - n_energy_labels / energy_noise + (KnK_e * Sigma).trace() / en3; - complexity_grad(hyp_index + 1) = - f_noise_one.sum() / force_noise + complexity_grad(hyp_index + 1) = - n_force_labels / force_noise + (KnK_f * Sigma).trace() / fn3; - complexity_grad(hyp_index + 2) = - s_noise_one.sum() / stress_noise + complexity_grad(hyp_index + 2) = - n_stress_labels / stress_noise + (KnK_s * Sigma).trace() / sn3; // Derivative of data_fit over noise @@ -1039,10 +1061,12 @@ SparseGP ::compute_likelihood_gradient(const Eigen::VectorXd &hyperparameters) { double sigma_f = hyperparameters(hyp_index + 1); double sigma_s = hyperparameters(hyp_index + 2); - Eigen::VectorXd noise_vec = sigma_e * sigma_e * e_noise_one + sigma_f * sigma_f * f_noise_one + sigma_s * sigma_s * s_noise_one; - Eigen::VectorXd e_noise_grad = 2 * sigma_e * e_noise_one; - Eigen::VectorXd f_noise_grad = 2 * sigma_f * f_noise_one; - Eigen::VectorXd s_noise_grad = 2 * sigma_s * s_noise_one; + Eigen::VectorXd noise_vec = sigma_e * sigma_e * inv_e_noise_one + + sigma_f * sigma_f * inv_f_noise_one + + sigma_s * sigma_s * inv_s_noise_one; + Eigen::VectorXd e_noise_grad = 2 * sigma_e * inv_e_noise_one; + Eigen::VectorXd f_noise_grad = 2 * sigma_f * inv_f_noise_one; + Eigen::VectorXd s_noise_grad = 2 * sigma_s * inv_s_noise_one; // Compute Qff and Qff grads. Eigen::MatrixXd Qff_plus_lambda = @@ -1236,7 +1260,7 @@ void SparseGP::write_varmap_coefficients( for (int i = 0; i < hyperparameters.size(); i++) { coeff_file << hyperparameters(i) << " "; } - coeff_file << "\n"; + coeff_file << "\n" << kernels[kernel_index]->kernel_name << "\n"; // Write descriptor information to file. int coeff_size = varmap_coeffs.row(0).size(); @@ -1387,7 +1411,11 @@ void SparseGP::write_sparse_descriptors( if (sparse_descriptors[i].descriptor_norms[s](j) < empty_thresh) { coeff_file << 0.0 << " "; } else { - coeff_file << sparse_descriptors[i].descriptors[s](j, k) / sparse_descriptors[i].descriptor_norms[s](j) << " "; + if (kernels[i]->kernel_name.find("NormalizedDotProduct") != std::string::npos) { + coeff_file << sparse_descriptors[i].descriptors[s](j, k) / sparse_descriptors[i].descriptor_norms[s](j) << " "; + } else { + coeff_file << sparse_descriptors[i].descriptors[s](j, k) << " "; + } } // Change line after writing 5 numbers diff --git a/src/flare_pp/bffs/sparse_gp.h b/src/flare_pp/bffs/sparse_gp.h index 18f81b3e9..2479796ca 100644 --- a/src/flare_pp/bffs/sparse_gp.h +++ b/src/flare_pp/bffs/sparse_gp.h @@ -34,6 +34,7 @@ class SparseGP { // Label attributes. Eigen::VectorXd noise_vector, y, label_count, e_noise_one, f_noise_one, s_noise_one; + Eigen::VectorXd inv_e_noise_one, inv_f_noise_one, inv_s_noise_one; int n_energy_labels = 0, n_force_labels = 0, n_stress_labels = 0, n_sparse = 0, n_labels = 0, n_strucs = 0; double energy_noise, force_noise, stress_noise; @@ -62,7 +63,7 @@ class SparseGP { std::vector> sort_clusters_by_uncertainty(const Structure &structure); - void add_training_structure(const Structure &structure, const std::vector atom_indices = {-1}); + void add_training_structure(const Structure &structure, const std::vector atom_indices = {-1}, double rel_e_noise = 1, double rel_f_noise = 1, double rel_s_noise = 1); void update_Kuu(const std::vector &cluster_descriptors); void update_Kuf(const std::vector &cluster_descriptors); void stack_Kuu(); diff --git a/src/flare_pp/kernels/dot_product.cpp b/src/flare_pp/kernels/dot_product.cpp new file mode 100644 index 000000000..f61482ef3 --- /dev/null +++ b/src/flare_pp/kernels/dot_product.cpp @@ -0,0 +1,870 @@ +#include "dot_product.h" +#include "descriptor.h" +#include "sparse_gp.h" +#include "structure.h" +#undef NDEBUG +#include +#include +#include // File operations +#include // setprecision +#include +#include + +DotProduct ::DotProduct(){}; + +DotProduct ::DotProduct(double sigma, double power) { + + this->sigma = sigma; + sig2 = sigma * sigma; + this->power = power; + kernel_name = "DotProduct"; + + // Set kernel hyperparameters. + Eigen::VectorXd hyps(1); + hyps << sigma; + kernel_hyperparameters = hyps; +} + +Eigen::MatrixXd DotProduct ::envs_envs(const ClusterDescriptor &envs1, + const ClusterDescriptor &envs2, + const Eigen::VectorXd &hyps) { + + // Set square of the signal variance. + double sig_sq = hyps(0) * hyps(0); + + // Check types. + int n_types_1 = envs1.n_types; + int n_types_2 = envs2.n_types; + assert(n_types_1 == n_types_2); + + // Check descriptor size. + int n_descriptors_1 = envs1.n_descriptors; + int n_descriptors_2 = envs2.n_descriptors; + assert(n_descriptors_1 == n_descriptors_2); + + Eigen::MatrixXd kern_mat = + Eigen::MatrixXd::Zero(envs1.n_clusters, envs2.n_clusters); + int n_types = n_types_1; + double empty_thresh = 1e-8; + + for (int s = 0; s < n_types; s++) { + // Why not do envs1.descriptors[s] / envs1.descriptor_norms[s] + // and then multiply them to get norm_dot matrix directly?? + // Compute dot products. (Should be done in parallel with MKL.) + Eigen::MatrixXd dot_vals = + envs1.descriptors[s] * envs2.descriptors[s].transpose(); + + // Compute kernels. + int n_sparse_1 = envs1.n_clusters_by_type[s]; + int c_sparse_1 = envs1.cumulative_type_count[s]; + int n_sparse_2 = envs2.n_clusters_by_type[s]; + int c_sparse_2 = envs2.cumulative_type_count[s]; + +#pragma omp parallel for + for (int i = 0; i < n_sparse_1; i++) { + double norm_i = envs1.descriptor_norms[s](i); + + // Continue if sparse environment i has no neighbors. + if (norm_i < empty_thresh) + continue; + int ind1 = c_sparse_1 + i; + + for (int j = 0; j < n_sparse_2; j++) { + double norm_j = envs2.descriptor_norms[s](j); + //double norm_ij = norm_i * norm_j; + + // Continue if atom j has no neighbors. + if (norm_j < empty_thresh) + continue; + int ind2 = c_sparse_2 + j; + + // Energy kernel. + double norm_dot = dot_vals(i, j); + kern_mat(ind1, ind2) += sig_sq * pow(norm_dot, power); + } + } + } + return kern_mat; +} + +std::vector +DotProduct ::envs_envs_grad(const ClusterDescriptor &envs1, + const ClusterDescriptor &envs2, + const Eigen::VectorXd &hyps) { + + std::vector grad_mats; + Eigen::MatrixXd kern = envs_envs(envs1, envs2, hyps); + Eigen::MatrixXd grad = 2 * kern / hyps(0); + grad_mats.push_back(kern); + grad_mats.push_back(grad); + return grad_mats; +} + +std::vector +DotProduct ::envs_struc_grad(const ClusterDescriptor &envs, + const DescriptorValues &struc, + const Eigen::VectorXd &hyps) { + + std::vector grad_mats; + Eigen::MatrixXd kern = envs_struc(envs, struc, hyps); + Eigen::MatrixXd grad = 2 * kern / hyps(0); + grad_mats.push_back(kern); + grad_mats.push_back(grad); + return grad_mats; +} + +Eigen::MatrixXd DotProduct ::envs_struc(const ClusterDescriptor &envs, + const DescriptorValues &struc, + const Eigen::VectorXd &hyps) { + + // Set square of the signal variance. + double sig_sq = hyps(0) * hyps(0); + + // Check types. + int n_types_1 = envs.n_types; + int n_types_2 = struc.n_types; + assert(n_types_1 == n_types_2); + + // Check descriptor size. + int n_descriptors_1 = envs.n_descriptors; + int n_descriptors_2 = struc.n_descriptors; + assert(n_descriptors_1 == n_descriptors_2); + + Eigen::MatrixXd kern_mat = + Eigen::MatrixXd::Zero(envs.n_clusters, 1 + struc.n_atoms * 3 + 6); + int n_types = envs.n_types; + double vol_inv = 1 / struc.volume; + double empty_thresh = 1e-8; + + for (int s = 0; s < n_types; s++) { + // Compute dot products. (Should be done in parallel with MKL.) + Eigen::MatrixXd dot_vals = + envs.descriptors[s] * struc.descriptors[s].transpose(); + Eigen::MatrixXd force_dot = + envs.descriptors[s] * struc.descriptor_force_dervs[s].transpose(); + + Eigen::VectorXd struc_force_dot = struc.descriptor_force_dots[s]; + + // Compute kernels. Can parallelize over environments. + int n_sparse = envs.n_clusters_by_type[s]; + int n_struc = struc.n_clusters_by_type[s]; + int c_sparse = envs.cumulative_type_count[s]; + +#pragma omp parallel for + for (int i = 0; i < n_sparse; i++) { + double norm_i = envs.descriptor_norms[s](i); + + // Continue if sparse environment i has no neighbors. + if (norm_i < empty_thresh) + continue; + int sparse_index = c_sparse + i; + + for (int j = 0; j < n_struc; j++) { + double norm_j = struc.descriptor_norms[s](j); + //double norm_ij = norm_i * norm_j; + //double norm_ij3 = norm_ij * norm_j * norm_j; + + // Continue if atom j has no neighbors. + if (norm_j < empty_thresh) + continue; + + // Energy kernel. + double norm_dot = dot_vals(i, j); // / norm_ij; + double dval = power * pow(norm_dot, power - 1); + kern_mat(sparse_index, 0) += sig_sq * pow(norm_dot, power); + + // Force kernel. + int n_neigh = struc.neighbor_counts[s](j); + int c_neigh = struc.cumulative_neighbor_counts[s](j); + int atom_index = struc.atom_indices[s](j); + + for (int k = 0; k < n_neigh; k++) { + int neighbor_index = struc.neighbor_indices[s](c_neigh + k); + int stress_counter = 0; + + for (int comp = 0; comp < 3; comp++) { + int ind = c_neigh + k; + int force_index = 3 * ind + comp; + double f1 = force_dot(i, force_index); // / norm_ij; + //double f2 = + // dot_vals(i, j) * struc_force_dot(force_index) / norm_ij3; + double f3 = f1; // - f2; + double force_kern_val = sig_sq * dval * f3; + + kern_mat(sparse_index, 1 + 3 * neighbor_index + comp) -= + force_kern_val; + kern_mat(sparse_index, 1 + 3 * atom_index + comp) += force_kern_val; + + for (int comp2 = comp; comp2 < 3; comp2++) { + double coord = struc.neighbor_coordinates[s](ind, comp2); + kern_mat(sparse_index, 1 + 3 * struc.n_atoms + stress_counter) -= + force_kern_val * coord * vol_inv; + stress_counter++; + } + } + } + } + } + } + + return kern_mat; +} + +Eigen::MatrixXd +DotProduct ::struc_struc(const DescriptorValues &struc1, + const DescriptorValues &struc2, + const Eigen::VectorXd &hyps) { + + //throw std::logic_error("struc_struc kernel for DotProduct is not implemented"); + + // Set square of the signal variance. + double sig_sq = hyps(0) * hyps(0); + + int n_elements_1 = 1 + 3 * struc1.n_atoms + 6; + int n_elements_2 = 1 + 3 * struc2.n_atoms + 6; + Eigen::MatrixXd kernel_matrix = + Eigen::MatrixXd::Zero(n_elements_1, n_elements_2); + + // Check types. + int n_types_1 = struc1.n_types; + int n_types_2 = struc2.n_types; + bool type_check = (n_types_1 == n_types_2); + assert(n_types_1 == n_types_2); + + // Check descriptor size. + int n_descriptors_1 = struc1.n_descriptors; + int n_descriptors_2 = struc2.n_descriptors; + assert(n_descriptors_1 == n_descriptors_2); + + double vol_inv_1 = 1 / struc1.volume; + double vol_inv_2 = 1 / struc2.volume; + + double empty_thresh = 1e-8; + std::vector stress_inds{0, 3, 5}; + + for (int s = 0; s < n_types_1; s++) { + // Compute dot products. + Eigen::MatrixXd dot_vals = + struc1.descriptors[s] * struc2.descriptors[s].transpose(); + Eigen::MatrixXd force_dot_1 = + struc1.descriptor_force_dervs[s] * struc2.descriptors[s].transpose(); + Eigen::MatrixXd force_dot_2 = + struc2.descriptor_force_dervs[s] * struc1.descriptors[s].transpose(); + Eigen::MatrixXd force_force = struc1.descriptor_force_dervs[s] * + struc2.descriptor_force_dervs[s].transpose(); + + Eigen::VectorXd struc_force_dot_1 = struc1.descriptor_force_dots[s]; + Eigen::VectorXd struc_force_dot_2 = struc2.descriptor_force_dots[s]; + + // Compute kernels. + int n_struc1 = struc1.n_clusters_by_type[s]; + int n_struc2 = struc2.n_clusters_by_type[s]; + + // TODO: Parallelize. + for (int i = 0; i < n_struc1; i++) { + double norm_i = struc1.descriptor_norms[s](i); + + // Continue if atom i has no neighbors. + if (norm_i < empty_thresh) + continue; + + double norm_i2 = norm_i * norm_i; + double norm_i3 = norm_i2 * norm_i; + + for (int j = 0; j < n_struc2; j++) { + double norm_j = struc2.descriptor_norms[s](j); + + // Continue if atom j has no neighbors. + if (norm_j < empty_thresh) + continue; + + double norm_j2 = norm_j * norm_j; + double norm_j3 = norm_j2 * norm_j; + double norm_ij = norm_i * norm_j; + + // Energy/energy kernel. + double norm_dot = dot_vals(i, j) / norm_ij; + double c1 = (power - 1) * power * pow(norm_dot, power - 2); + if (abs(norm_dot) < empty_thresh && power < 2) { + throw std::invalid_argument( "Dot product of descriptors is 0, \ + and the negative power function in force-force kernel diverges." ); + } + double c2 = power * pow(norm_dot, power - 1); + kernel_matrix(0, 0) += sig_sq * pow(norm_dot, power); + + int n_neigh_1 = struc1.neighbor_counts[s](i); + int c_neigh_1 = struc1.cumulative_neighbor_counts[s](i); + int c_ind_1 = struc1.atom_indices[s](i); + + int n_neigh_2 = struc2.neighbor_counts[s](j); + int c_neigh_2 = struc2.cumulative_neighbor_counts[s](j); + int c_ind_2 = struc2.atom_indices[s](j); + + // Energy/force and energy/stress kernels. + for (int k = 0; k < n_neigh_2; k++) { + int ind = c_neigh_2 + k; + int neighbor_index = struc2.neighbor_indices[s](ind); + int stress_counter = 0; + + for (int comp = 0; comp < 3; comp++) { + int force_index = 3 * ind + comp; + double f1 = force_dot_2(force_index, i) / norm_ij; + double f2 = dot_vals(i, j) * struc_force_dot_2(force_index) / + (norm_i * norm_j3); + double f3 = f1 - f2; + double force_kern_val = sig_sq * c2 * f3; + + // Energy/force. + kernel_matrix(0, 1 + 3 * neighbor_index + comp) -= force_kern_val; + kernel_matrix(0, 1 + 3 * c_ind_2 + comp) += force_kern_val; + + // Energy/stress. + for (int comp2 = comp; comp2 < 3; comp2++) { + double coord = struc2.neighbor_coordinates[s](ind, comp2); + kernel_matrix(0, 1 + 3 * struc2.n_atoms + stress_counter) -= + force_kern_val * coord * vol_inv_2; + stress_counter++; + } + } + } + + // Force/energy and stress/energy kernels. + for (int k = 0; k < n_neigh_1; k++) { + int ind = c_neigh_1 + k; + int neighbor_index = struc1.neighbor_indices[s](ind); + int stress_counter = 0; + + for (int comp = 0; comp < 3; comp++) { + int force_index = 3 * ind + comp; + double f1 = force_dot_1(force_index, j) / norm_ij; + double f2 = dot_vals(i, j) * struc_force_dot_1(force_index) / + (norm_j * norm_i3); + double f3 = f1 - f2; + double force_kern_val = sig_sq * c2 * f3; + + // Force/energy. + kernel_matrix(1 + 3 * neighbor_index + comp, 0) -= force_kern_val; + kernel_matrix(1 + 3 * c_ind_1 + comp, 0) += force_kern_val; + + // Stress/energy. + for (int comp2 = comp; comp2 < 3; comp2++) { + double coord = struc1.neighbor_coordinates[s](ind, comp2); + kernel_matrix(1 + 3 * struc1.n_atoms + stress_counter, 0) -= + force_kern_val * coord * vol_inv_1; + stress_counter++; + } + } + } + + // Force/force, force/stress, stress/force, and stress/stress kernels. + for (int k = 0; k < n_neigh_1; k++) { + int ind1 = c_neigh_1 + k; + int n_ind_1 = struc1.neighbor_indices[s](ind1); + + for (int l = 0; l < n_neigh_2; l++) { + int ind2 = c_neigh_2 + l; + int n_ind_2 = struc2.neighbor_indices[s](ind2); + + for (int m = 0; m < 3; m++) { + int f_ind_1 = 3 * ind1 + m; + for (int n = 0; n < 3; n++) { + int f_ind_2 = 3 * ind2 + n; + double v1 = force_dot_1(f_ind_1, j) / norm_ij - + norm_dot * struc_force_dot_1(f_ind_1) / norm_i2; + double v2 = force_dot_2(f_ind_2, i) / norm_ij - + norm_dot * struc_force_dot_2(f_ind_2) / norm_j2; + double v3 = force_force(f_ind_1, f_ind_2) / norm_ij; + double v4 = struc_force_dot_1(f_ind_1) * + force_dot_2(f_ind_2, i) / (norm_i3 * norm_j); + double v5 = struc_force_dot_2(f_ind_2) * + force_dot_1(f_ind_1, j) / (norm_i * norm_j3); + double v6 = struc_force_dot_1(f_ind_1) * + struc_force_dot_2(f_ind_2) * norm_dot / + (norm_i2 * norm_j2); + + double kern_val = + sig_sq * (c1 * v1 * v2 + c2 * (v3 - v4 - v5 + v6)); + + // Force/force. + kernel_matrix(1 + c_ind_1 * 3 + m, 1 + c_ind_2 * 3 + n) += + kern_val; + kernel_matrix(1 + c_ind_1 * 3 + m, 1 + n_ind_2 * 3 + n) -= + kern_val; + kernel_matrix(1 + n_ind_1 * 3 + m, 1 + c_ind_2 * 3 + n) -= + kern_val; + kernel_matrix(1 + n_ind_1 * 3 + m, 1 + n_ind_2 * 3 + n) += + kern_val; + + // Stress/force. + int stress_ind_1 = stress_inds[m]; + for (int p = m; p < 3; p++) { + double coord = struc1.neighbor_coordinates[s](ind1, p); + kernel_matrix(1 + 3 * struc1.n_atoms + stress_ind_1, + 1 + c_ind_2 * 3 + n) -= + kern_val * coord * vol_inv_1; + kernel_matrix(1 + 3 * struc1.n_atoms + stress_ind_1, + 1 + n_ind_2 * 3 + n) += + kern_val * coord * vol_inv_1; + stress_ind_1++; + } + + // Force/stress. + int stress_ind_2 = stress_inds[n]; + for (int p = n; p < 3; p++) { + double coord = struc2.neighbor_coordinates[s](ind2, p); + kernel_matrix(1 + c_ind_1 * 3 + m, + 1 + 3 * struc2.n_atoms + stress_ind_2) -= + kern_val * coord * vol_inv_2; + kernel_matrix(1 + n_ind_1 * 3 + m, + 1 + 3 * struc2.n_atoms + stress_ind_2) += + kern_val * coord * vol_inv_2; + stress_ind_2++; + } + + // Stress/stress. + stress_ind_1 = stress_inds[m]; + for (int p = m; p < 3; p++) { + double coord1 = struc1.neighbor_coordinates[s](ind1, p); + stress_ind_2 = stress_inds[n]; + for (int q = n; q < 3; q++) { + double coord2 = struc2.neighbor_coordinates[s](ind2, q); + kernel_matrix(1 + 3 * struc1.n_atoms + stress_ind_1, + 1 + 3 * struc2.n_atoms + stress_ind_2) += + kern_val * coord1 * coord2 * vol_inv_1 * vol_inv_2; + stress_ind_2++; + } + stress_ind_1++; + } + } + } + } + } + } + } + } + + return kernel_matrix; +} + +Eigen::VectorXd +DotProduct ::self_kernel_struc(const DescriptorValues &struc, + const Eigen::VectorXd &hyps) { + + double sig_sq = hyps(0) * hyps(0); + + int n_elements = 1 + 3 * struc.n_atoms + 6; + Eigen::VectorXd kernel_vector = Eigen::VectorXd::Zero(n_elements); + + int n_types = struc.n_types; + double vol_inv = 1 / struc.volume; + double vol_inv_sq = vol_inv * vol_inv; + double empty_thresh = 1e-8; + + for (int s = 0; s < n_types; s++) { + // Compute dot products. (Should be done in parallel with MKL.) + Eigen::MatrixXd dot_vals = + struc.descriptors[s] * struc.descriptors[s].transpose(); + Eigen::MatrixXd force_dot = + struc.descriptor_force_dervs[s] * struc.descriptors[s].transpose(); + Eigen::MatrixXd force_force = struc.descriptor_force_dervs[s] * + struc.descriptor_force_dervs[s].transpose(); + + Eigen::VectorXd struc_force_dot = struc.descriptor_force_dots[s]; + + // Compute kernels. + int n_struc = struc.n_clusters_by_type[s]; + + // TODO: Parallelize. + Eigen::MatrixXd par_mat = Eigen::MatrixXd::Zero(n_struc, n_elements); +#pragma omp parallel for + for (int i = 0; i < n_struc; i++) { + double norm_i = struc.descriptor_norms[s](i); + + // Continue if atom i has no neighbors. + if (norm_i < empty_thresh) + continue; + + double norm_i2 = norm_i * norm_i; + double norm_i3 = norm_i2 * norm_i; + + for (int j = i; j < n_struc; j++) { + double norm_j = struc.descriptor_norms[s](j); + + // Continue if atom j has no neighbors. + if (norm_j < empty_thresh) + continue; + + double mult_fac; + if (i == j) + mult_fac = 1; + else + mult_fac = 2; + + double norm_j2 = norm_j * norm_j; + double norm_j3 = norm_j2 * norm_j; + double norm_ij = norm_i * norm_j; + + // Energy kernel. + double norm_dot = dot_vals(i, j) / norm_ij; + double c1 = (power - 1) * power * pow(norm_dot, power - 2); + if (abs(norm_dot) < empty_thresh && power < 2) { + throw std::invalid_argument( "Dot product of descriptors is 0, \ + and the negative power function in force-force kernel diverges." ); + } + double c2 = power * pow(norm_dot, power - 1); + par_mat(i, 0) += sig_sq * mult_fac * pow(norm_dot, power); + + // Force kernel. + int n_neigh_1 = struc.neighbor_counts[s](i); + int c_neigh_1 = struc.cumulative_neighbor_counts[s](i); + int c_ind_1 = struc.atom_indices[s](i); + + int n_neigh_2 = struc.neighbor_counts[s](j); + int c_neigh_2 = struc.cumulative_neighbor_counts[s](j); + int c_ind_2 = struc.atom_indices[s](j); + + for (int k = 0; k < n_neigh_1; k++) { + int ind1 = c_neigh_1 + k; + int n_ind_1 = struc.neighbor_indices[s](ind1); + + for (int l = 0; l < n_neigh_2; l++) { + int ind2 = c_neigh_2 + l; + int n_ind_2 = struc.neighbor_indices[s](ind2); + + int stress_counter = 0; + for (int m = 0; m < 3; m++) { + int f_ind_1 = 3 * ind1 + m; + int f_ind_2 = 3 * ind2 + m; + double v1 = force_dot(f_ind_1, j) / norm_ij - + norm_dot * struc_force_dot(f_ind_1) / norm_i2; + double v2 = force_dot(f_ind_2, i) / norm_ij - + norm_dot * struc_force_dot(f_ind_2) / norm_j2; + double v3 = force_force(f_ind_1, f_ind_2) / norm_ij; + double v4 = struc_force_dot(f_ind_1) * force_dot(f_ind_2, i) / + (norm_i3 * norm_j); + double v5 = struc_force_dot(f_ind_2) * force_dot(f_ind_1, j) / + (norm_i * norm_j3); + double v6 = struc_force_dot(f_ind_1) * struc_force_dot(f_ind_2) * + norm_dot / (norm_i2 * norm_j2); + + double kern_val = + sig_sq * mult_fac * (c1 * v1 * v2 + c2 * (v3 - v4 - v5 + v6)); + + if (c_ind_1 == c_ind_2) + par_mat(i, 1 + c_ind_1 * 3 + m) += kern_val; + if (c_ind_1 == n_ind_2) + par_mat(i, 1 + c_ind_1 * 3 + m) -= kern_val; + if (n_ind_1 == c_ind_2) + par_mat(i, 1 + n_ind_1 * 3 + m) -= kern_val; + if (n_ind_1 == n_ind_2) + par_mat(i, 1 + n_ind_1 * 3 + m) += kern_val; + + // Stress kernel. + for (int n = m; n < 3; n++) { + double coord1 = struc.neighbor_coordinates[s](ind1, n); + double coord2 = struc.neighbor_coordinates[s](ind2, n); + par_mat(i, 1 + 3 * struc.n_atoms + stress_counter) += + kern_val * coord1 * coord2 * vol_inv_sq; + stress_counter++; + } + } + } + } + } + } + + // Reduce kernel values. + for (int i = 0; i < n_struc; i++) { + for (int j = 0; j < n_elements; j++) { + kernel_vector(j) += par_mat(i, j); + } + } + } + + return kernel_vector; +} + +std::vector +DotProduct ::Kuu_grad(const ClusterDescriptor &envs, + const Eigen::MatrixXd &Kuu, + const Eigen::VectorXd &new_hyps) { + + std::vector kernel_gradients; + + // Compute Kuu. + Eigen::MatrixXd Kuu_new = Kuu * (new_hyps(0) * new_hyps(0) / sig2); + kernel_gradients.push_back(Kuu_new); + + // Compute sigma gradient. + Eigen::MatrixXd sigma_gradient = Kuu * (2 * new_hyps(0) / sig2); + kernel_gradients.push_back(sigma_gradient); + + return kernel_gradients; +} + +std::vector +DotProduct ::Kuf_grad(const ClusterDescriptor &envs, + const std::vector &strucs, + int kernel_index, const Eigen::MatrixXd &Kuf, + const Eigen::VectorXd &new_hyps) { + + std::vector kernel_gradients; + + // Compute Kuf. + Eigen::MatrixXd Kuf_new = Kuf * (new_hyps(0) * new_hyps(0) / sig2); + kernel_gradients.push_back(Kuf_new); + + // Compute sigma gradient. + Eigen::MatrixXd sigma_gradient = Kuf * (2 * new_hyps(0) / sig2); + kernel_gradients.push_back(sigma_gradient); + + return kernel_gradients; +} + +void DotProduct ::set_hyperparameters(Eigen::VectorXd new_hyps) { + sigma = new_hyps(0); + sig2 = sigma * sigma; + kernel_hyperparameters = new_hyps; +} + +Eigen::MatrixXd +DotProduct ::compute_mapping_coefficients(const SparseGP &gp_model, + int kernel_index) { + + // Assumes there is at least one sparse environment stored in the sparse GP. + + Eigen::MatrixXd mapping_coeffs; + + if (power == 1) { + mapping_coeffs = compute_map_coeff_pow1(gp_model, kernel_index); + } else if (power == 2) { + mapping_coeffs = compute_map_coeff_pow2(gp_model, kernel_index); + } else { + std::cout + << "Mapping coefficients of the dot product kernel are " + "implemented for power 2 only." + << std::endl; + } + + return mapping_coeffs; +} + +Eigen::MatrixXd +DotProduct ::compute_map_coeff_pow1(const SparseGP &gp_model, + int kernel_index) { + + // Initialize beta vector. + int p_size = gp_model.sparse_descriptors[kernel_index].n_descriptors; + int beta_size = p_size; + int n_species = gp_model.sparse_descriptors[kernel_index].n_types; + int n_sparse = gp_model.sparse_descriptors[kernel_index].n_clusters; + Eigen::MatrixXd mapping_coeffs = Eigen::MatrixXd::Zero(n_species, beta_size); + double empty_thresh = 1e-8; + + // Get alpha index. + int alpha_ind = 0; + for (int i = 0; i < kernel_index; i++) { + alpha_ind += gp_model.sparse_descriptors[i].n_clusters; + } + + // Loop over types. + for (int i = 0; i < n_species; i++) { + int n_types = + gp_model.sparse_descriptors[kernel_index].n_clusters_by_type[i]; + int c_types = + gp_model.sparse_descriptors[kernel_index].cumulative_type_count[i]; + + // Loop over clusters within each type. + for (int j = 0; j < n_types; j++) { + Eigen::VectorXd p_current = + gp_model.sparse_descriptors[kernel_index].descriptors[i].row(j); + double p_norm = + gp_model.sparse_descriptors[kernel_index].descriptor_norms[i](j); + + // Skip empty environments. + if (p_norm < empty_thresh) + continue; + + double alpha_val = gp_model.alpha(alpha_ind + c_types + j); + int beta_count = 0; + + // First loop over descriptor values. + for (int k = 0; k < p_size; k++) { + double p_ik = p_current(k); // / p_norm; + mapping_coeffs(i, beta_count) += sig2 * p_ik * alpha_val; + + beta_count++; + } + } + } + + return mapping_coeffs; +} + +Eigen::MatrixXd +DotProduct ::compute_map_coeff_pow2(const SparseGP &gp_model, + int kernel_index) { + + // Initialize beta vector. + int p_size = gp_model.sparse_descriptors[kernel_index].n_descriptors; + int beta_size = p_size * (p_size + 1) / 2; + int n_species = gp_model.sparse_descriptors[kernel_index].n_types; + int n_sparse = gp_model.sparse_descriptors[kernel_index].n_clusters; + Eigen::MatrixXd mapping_coeffs = Eigen::MatrixXd::Zero(n_species, beta_size); + double empty_thresh = 1e-8; + + // Get alpha index. + int alpha_ind = 0; + for (int i = 0; i < kernel_index; i++) { + alpha_ind += gp_model.sparse_descriptors[i].n_clusters; + } + + // Loop over types. + for (int i = 0; i < n_species; i++) { + int n_types = + gp_model.sparse_descriptors[kernel_index].n_clusters_by_type[i]; + int c_types = + gp_model.sparse_descriptors[kernel_index].cumulative_type_count[i]; + + // Loop over clusters within each type. + for (int j = 0; j < n_types; j++) { + Eigen::VectorXd p_current = + gp_model.sparse_descriptors[kernel_index].descriptors[i].row(j); + double p_norm = + gp_model.sparse_descriptors[kernel_index].descriptor_norms[i](j); + + // Skip empty environments. + if (p_norm < empty_thresh) + continue; + + double alpha_val = gp_model.alpha(alpha_ind + c_types + j); + int beta_count = 0; + + // First loop over descriptor values. + for (int k = 0; k < p_size; k++) { + double p_ik = p_current(k); // / p_norm; + + // Second loop over descriptor values. + for (int l = k; l < p_size; l++) { + double p_il = p_current(l); // / p_norm; + double beta_val = sig2 * p_ik * p_il * alpha_val; + + // Update beta vector. + if (k != l) { + mapping_coeffs(i, beta_count) += 2 * beta_val; + } else { + mapping_coeffs(i, beta_count) += beta_val; + } + + beta_count++; + } + } + } + } + + return mapping_coeffs; +} + +Eigen::MatrixXd DotProduct ::compute_varmap_coefficients( + const SparseGP &gp_model, int kernel_index){ + + // Assumes there is at least one sparse environment stored in the sparse GP. + + Eigen::MatrixXd mapping_coeffs; + if (power != 1){ + std::cout + << "Mapping coefficients of the normalized dot product kernel are " + "implemented for power 1 only." + << std::endl; + return mapping_coeffs; + } + + double empty_thresh = 1e-8; + + // Initialize beta vector. + int p_size = gp_model.sparse_descriptors[kernel_index].n_descriptors; + int beta_size = p_size * (p_size + 1) / 2; + int n_species = gp_model.sparse_descriptors[kernel_index].n_types; + int n_sparse = gp_model.sparse_descriptors[kernel_index].n_clusters; + //mapping_coeffs = Eigen::MatrixXd::Zero(n_species * n_species, p_size * p_size); // can be reduced by symmetry + mapping_coeffs = Eigen::MatrixXd::Zero(n_species, p_size * p_size); // can be reduced by symmetry + + // Get alpha index. + + int alpha_ind = 0; + for (int i = 0; i < kernel_index; i++){ + alpha_ind += gp_model.sparse_descriptors[i].n_clusters; + } + + // Loop over types. + for (int s = 0; s < n_species; s++){ + int n_types = gp_model.sparse_descriptors[kernel_index].n_clusters_by_type[s]; + int c_types = + gp_model.sparse_descriptors[kernel_index].cumulative_type_count[s]; + int K_ind = alpha_ind + c_types; + + // Loop over clusters within each type. + for (int i = 0; i < n_types; i++){ + Eigen::VectorXd pi_current = + gp_model.sparse_descriptors[kernel_index].descriptors[s].row(i); + double pi_norm = + gp_model.sparse_descriptors[kernel_index].descriptor_norms[s](i); + + // Skip empty environments. + if (pi_norm < empty_thresh) + continue; + + // TODO: include symmetry of i & j + // Loop over clusters within each type. + for (int j = 0; j < n_types; j++){ + Eigen::VectorXd pj_current = + gp_model.sparse_descriptors[kernel_index].descriptors[s].row(j); + double pj_norm = + gp_model.sparse_descriptors[kernel_index].descriptor_norms[s](j); + + // Skip empty environments. + if (pj_norm < empty_thresh) + continue; + + double Kuu_inv_ij = gp_model.Kuu_inverse(K_ind + i, K_ind + j); + double Kuu_inv_ij_normed = Kuu_inv_ij; // / pi_norm / pj_norm; +// double Sigma_ij = gp_model.Sigma(K_ind + i, K_ind + j); +// double Sigma_ij_normed = Sigma_ij / pi_norm / pj_norm; + int beta_count = 0; + + // First loop over descriptor values. + for (int k = 0; k < p_size; k++) { + double p_ik = pi_current(k); + + // Second loop over descriptor values. + for (int l = 0; l < p_size; l++){ + double p_jl = pj_current(l); + + // Update beta vector. + double beta_val = sig2 * sig2 * p_ik * p_jl * (- Kuu_inv_ij_normed); // + Sigma_ij_normed); // To match the compute_cluster_uncertainty function + mapping_coeffs(s, beta_count) += beta_val; + + if (k == l && i == 0 && j == 0) { + mapping_coeffs(s, beta_count) += sig2; // the self kernel term + } + + beta_count++; + } + } + } + } + } + + return mapping_coeffs; +} + +void DotProduct ::write_info(std::ofstream &coeff_file) { + coeff_file << std::fixed << std::setprecision(0); + coeff_file << power << " DotProduct\n"; +} + +nlohmann::json DotProduct ::return_json(){ + nlohmann::json j; + to_json(j, *this); + return j; +} diff --git a/src/flare_pp/kernels/dot_product.h b/src/flare_pp/kernels/dot_product.h new file mode 100644 index 000000000..d53fc8348 --- /dev/null +++ b/src/flare_pp/kernels/dot_product.h @@ -0,0 +1,74 @@ +#ifndef DOT_PRODUCT_H +#define DOT_PRODUCT_H + +#include "kernel.h" +#include +#include + +class DescriptorValues; +class ClusterDescriptor; + +class DotProduct : public Kernel { +public: + double sigma, sig2, power; + + DotProduct(); + + DotProduct(double sigma, double power); + + Eigen::MatrixXd envs_envs(const ClusterDescriptor &envs1, + const ClusterDescriptor &envs2, + const Eigen::VectorXd &hyps); + + std::vector envs_envs_grad(const ClusterDescriptor &envs1, + const ClusterDescriptor &envs2, + const Eigen::VectorXd &hyps); + + Eigen::MatrixXd envs_struc(const ClusterDescriptor &envs, + const DescriptorValues &struc, + const Eigen::VectorXd &hyps); + + std::vector envs_struc_grad(const ClusterDescriptor &envs, + const DescriptorValues &struc, + const Eigen::VectorXd &hyps); + + Eigen::VectorXd self_kernel_struc(const DescriptorValues &struc, + const Eigen::VectorXd &hyps); + + Eigen::MatrixXd struc_struc(const DescriptorValues &struc1, + const DescriptorValues &struc2, + const Eigen::VectorXd &hyps); + + // Because of the simplicity of this kernel, Kuu_grad and Kuf_grad can + // be significantly accelerated over the default implementation, which + // reconstructs the covariance matrices from scratch. + std::vector Kuu_grad(const ClusterDescriptor &envs, + const Eigen::MatrixXd &Kuu, + const Eigen::VectorXd &new_hyps); + + std::vector Kuf_grad(const ClusterDescriptor &envs, + const std::vector &strucs, + int kernel_index, + const Eigen::MatrixXd &Kuf, + const Eigen::VectorXd &new_hyps); + + void set_hyperparameters(Eigen::VectorXd new_hyps); + + Eigen::MatrixXd compute_map_coeff_pow1(const SparseGP &gp_model, + int kernel_index); + Eigen::MatrixXd compute_map_coeff_pow2(const SparseGP &gp_model, + int kernel_index); + + Eigen::MatrixXd compute_mapping_coefficients(const SparseGP &gp_model, + int kernel_index); + Eigen::MatrixXd compute_varmap_coefficients(const SparseGP &gp_model, + int kernel_index); + void write_info(std::ofstream &coeff_file); + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(DotProduct, + sigma, sig2, power, kernel_name, kernel_hyperparameters) + + nlohmann::json return_json(); +}; + +#endif diff --git a/src/flare_pp/kernels/kernel.cpp b/src/flare_pp/kernels/kernel.cpp index ea822b2a7..d876b55d1 100644 --- a/src/flare_pp/kernels/kernel.cpp +++ b/src/flare_pp/kernels/kernel.cpp @@ -5,6 +5,7 @@ #include "normalized_dot_product.h" #include "norm_dot_icm.h" #include "squared_exponential.h" +#include "dot_product.h" Kernel ::Kernel(){}; Kernel ::Kernel(Eigen::VectorXd kernel_hyperparameters) { @@ -116,6 +117,11 @@ void from_json(const nlohmann::json& j, std::vector & kernels){ *sq_exp = j_kernel; kernels.push_back(sq_exp); } + else if (kernel_name == "DotProduct"){ + DotProduct* dotprod = new DotProduct; + *dotprod = j_kernel; + kernels.push_back(dotprod); + } else{ kernels.push_back(nullptr); } diff --git a/src/flare_pp/kernels/norm_dot_icm.cpp b/src/flare_pp/kernels/norm_dot_icm.cpp index 5762ceb34..74df15bd8 100644 --- a/src/flare_pp/kernels/norm_dot_icm.cpp +++ b/src/flare_pp/kernels/norm_dot_icm.cpp @@ -17,6 +17,7 @@ NormalizedDotProduct_ICM ::NormalizedDotProduct_ICM( sig2 = sigma * sigma; this->power = power; this->icm_coeffs = icm_coeffs; + kernel_name = "NormalizedDotProduct_ICM"; // Set kernel hyperparameters. no_types = icm_coeffs.rows(); diff --git a/src/flare_pp/kernels/norm_dot_icm.h b/src/flare_pp/kernels/norm_dot_icm.h index e36dea1ca..69da26534 100644 --- a/src/flare_pp/kernels/norm_dot_icm.h +++ b/src/flare_pp/kernels/norm_dot_icm.h @@ -13,7 +13,6 @@ class NormalizedDotProduct_ICM : public Kernel { double sigma, sig2, power; int no_types, n_icm_coeffs; Eigen::MatrixXd icm_coeffs; - std::string kernel_name = "NormalizedDotProduct_ICM"; NormalizedDotProduct_ICM(); diff --git a/src/flare_pp/kernels/normalized_dot_product.cpp b/src/flare_pp/kernels/normalized_dot_product.cpp index 60e11ccb0..caecef892 100644 --- a/src/flare_pp/kernels/normalized_dot_product.cpp +++ b/src/flare_pp/kernels/normalized_dot_product.cpp @@ -17,6 +17,7 @@ NormalizedDotProduct ::NormalizedDotProduct(double sigma, double power) { this->sigma = sigma; sig2 = sigma * sigma; this->power = power; + kernel_name = "NormalizedDotProduct"; // Set kernel hyperparameters. Eigen::VectorXd hyps(1); @@ -857,7 +858,7 @@ Eigen::MatrixXd NormalizedDotProduct ::compute_varmap_coefficients( void NormalizedDotProduct ::write_info(std::ofstream &coeff_file) { coeff_file << std::fixed << std::setprecision(0); - coeff_file << power << "\n"; + coeff_file << power << " NormalizedDotProduct\n"; } nlohmann::json NormalizedDotProduct ::return_json(){ diff --git a/src/flare_pp/kernels/normalized_dot_product.h b/src/flare_pp/kernels/normalized_dot_product.h index e87fb4d69..7fe14ef9c 100644 --- a/src/flare_pp/kernels/normalized_dot_product.h +++ b/src/flare_pp/kernels/normalized_dot_product.h @@ -11,7 +11,6 @@ class ClusterDescriptor; class NormalizedDotProduct : public Kernel { public: double sigma, sig2, power; - std::string kernel_name = "NormalizedDotProduct"; NormalizedDotProduct(); diff --git a/src/flare_pp/kernels/squared_exponential.cpp b/src/flare_pp/kernels/squared_exponential.cpp index bcaf39615..4fb0a87f7 100644 --- a/src/flare_pp/kernels/squared_exponential.cpp +++ b/src/flare_pp/kernels/squared_exponential.cpp @@ -15,6 +15,7 @@ SquaredExponential ::SquaredExponential(double sigma, double ls) { this->ls = ls; sig2 = sigma * sigma; ls2 = ls * ls; + kernel_name = "SquaredExponential"; // Set kernel hyperparameters. Eigen::VectorXd hyps(2); diff --git a/src/flare_pp/kernels/squared_exponential.h b/src/flare_pp/kernels/squared_exponential.h index 1b5f791f8..dd15f4db8 100644 --- a/src/flare_pp/kernels/squared_exponential.h +++ b/src/flare_pp/kernels/squared_exponential.h @@ -11,7 +11,6 @@ class ClusterDescriptor; class SquaredExponential : public Kernel { public: double sigma, ls, sig2, ls2; - std::string kernel_name = "SquaredExponential"; SquaredExponential(); diff --git a/tests/get_sgp.py b/tests/get_sgp.py index 4fef958c2..49c8f4a89 100644 --- a/tests/get_sgp.py +++ b/tests/get_sgp.py @@ -1,5 +1,5 @@ import numpy as np -from flare.bffs.sgp._C_flare import NormalizedDotProduct, B2 +from flare.bffs.sgp._C_flare import NormalizedDotProduct, DotProduct, B2 from flare.bffs.sgp import SGP_Wrapper from flare.bffs.sgp.calculator import SGP_Calculator from flare.atoms import FLARE_Atoms @@ -10,12 +10,13 @@ # Define kernel. sigma = 2.0 power = 1.0 -kernel = NormalizedDotProduct(sigma, power) +dotprod_kernel = DotProduct(sigma, power) +normdotprod_kernel = NormalizedDotProduct(sigma, power) # Define remaining parameters for the SGP wrapper. -sigma_e = 0.001 -sigma_f = 0.05 -sigma_s = 0.006 +sigma_e = 0.3 +sigma_f = 0.2 +sigma_s = 0.1 species_map = {6: 0, 8: 1} single_atom_energies = {0: -5, 1: -6} variance_type = "local" @@ -60,7 +61,12 @@ def get_isolated_atoms(numbers=[6, 8]): return flare_atoms -def get_empty_sgp(n_types=2, power=2, multiple_cutoff=False): +def get_empty_sgp(n_types=2, power=2, multiple_cutoff=False, kernel_type="NormalizedDotProduct"): + if kernel_type == "NormalizedDotProduct": + kernel = normdotprod_kernel + elif kernel_type == "DotProduct": + kernel = dotprod_kernel + kernel.power = power # Define B2 calculator. @@ -101,13 +107,13 @@ def get_empty_sgp(n_types=2, power=2, multiple_cutoff=False): return empty_sgp -def get_updated_sgp(n_types=2, power=2, multiple_cutoff=False): +def get_updated_sgp(n_types=2, power=2, multiple_cutoff=False, kernel_type="NormalizedDotProduct"): if n_types == 1: numbers = [6, 6] elif n_types == 2: numbers = [6, 8] - sgp = get_empty_sgp(n_types, power, multiple_cutoff) + sgp = get_empty_sgp(n_types, power, multiple_cutoff, kernel_type) # add a random structure to the training set training_structure = get_random_atoms(numbers=numbers) @@ -124,6 +130,9 @@ def get_updated_sgp(n_types=2, power=2, multiple_cutoff=False): energy=energy, stress=stress, mode="specific", + rel_e_noise=0.1, + rel_f_noise=0.2, + rel_s_noise=0.1, ) # add an isolated atom to the training data @@ -148,8 +157,8 @@ def get_updated_sgp(n_types=2, power=2, multiple_cutoff=False): return sgp -def get_sgp_calc(n_types=2, power=2, multiple_cutoff=False): - sgp = get_updated_sgp(n_types, power, multiple_cutoff) +def get_sgp_calc(n_types=2, power=2, multiple_cutoff=False, kernel_type="NormalizedDotProduct"): + sgp = get_updated_sgp(n_types, power, multiple_cutoff, kernel_type) sgp_calc = SGP_Calculator(sgp) return sgp_calc diff --git a/tests/test_lammps.py b/tests/test_lammps.py index a872a2098..fb7729678 100644 --- a/tests/test_lammps.py +++ b/tests/test_lammps.py @@ -136,13 +136,14 @@ def test_write_potential(n_species, n_types, power, struc, multicut, n_cpus): @pytest.mark.parametrize("struc", struc_list) @pytest.mark.parametrize("multicut", [False, True]) @pytest.mark.parametrize("n_cpus", n_cpus_list) +@pytest.mark.parametrize("kernel_type", ["NormalizedDotProduct", "DotProduct"]) def test_lammps_uncertainty( - n_species, n_types, use_map, power, struc, multicut, n_cpus + n_species, n_types, use_map, power, struc, multicut, n_cpus, kernel_type, ): if n_species > n_types: pytest.skip() - if (power == 1) and ("kokkos" in os.environ.get("lmp")): + if (power == 1 or kernel_type=="DotProduct") and ("kokkos" in os.environ.get("lmp")): pytest.skip() os.chdir(rootdir) @@ -153,7 +154,7 @@ def test_lammps_uncertainty( print(lmp_command) # get sgp & dump coefficient files - sgp_model = get_sgp_calc(n_types, power, multicut) + sgp_model = get_sgp_calc(n_types, power, multicut, kernel_type) contributor = "YX" kernel_index = 0 @@ -210,7 +211,7 @@ def test_lammps_uncertainty( ### run fix fix_nve all nve compute unc all flare/std/atom {coeff_str} -dump dump_all all custom 1 traj.lammps id type x y z vx vy vz fx fy fz c_unc +dump dump_all all custom 1 traj.lammps id type x y z vx vy vz fx fy fz c_unc dump_modify dump_all sort id thermo_style custom step temp press cpu pxx pyy pzz pxy pxz pyz ke pe etotal vol lx ly lz atoms thermo_modify flush yes format float %23.16g @@ -245,7 +246,7 @@ def test_lammps_uncertainty( # print(sgp_stds) # print(lmp_stds) print(sgp_model.gp_model.hyps) - assert np.allclose(sgp_stds[:, 0], lmp_stds.squeeze(), atol=1e-4) + assert np.allclose(sgp_stds[:, 0], lmp_stds.squeeze(), rtol=2e-3, atol=3e-3) os.chdir("..") os.system("rm -r tmp *.txt")